# A2A Conversation-First Layout: Identity Colors, Connection States, Transcript Motion, and Empty States
## Overview
The A2A page now features a conversation-first design with three major enhancements:
1.**Agent identity colors** — every agent is assigned a team-scoped color bucket (Backend, Frontend, UX/UI, Board, CEO, System)
2.**Collapsible context pane** (xl:+ breakpoint) — participant identity cards, linked task summary, and a no-task hint
3.**Live connection states** — four distinct visual states (connected, connecting, reconnecting, disconnected) with dismissable banners
4.**Transcript entrance motion** — new rows fade in with `transform`/`opacity` transitions, guarded by `prefers-reduced-motion`
5.**Split empty/error states** — distinct messaging for "no conversation selected", "selected but empty", and "fetch failed with retry"
This document covers the component API, integration, and usage patterns.
## Agent Team Colors
### Overview
Agent colors are scoped to **team** (cell), not per-agent, because it remains legible at 22-agent scale and never requires new tokens when a cell grows.
Resolves an agent slug (or UUID) to its team color by inspecting the slug prefix. Unknown slugs fall back to `system` — color is a scanning aid, never critical, so unknown agents don't throw.
Maps each team color to a pre-composed Tailwind class string with `bg-{color}/15`, `border-{color}/40`, and `text-{color}` weights for light and dark modes.
The context pane renders participant identity cards, linked task summary, and a no-task hint at the `xl:` breakpoint and above. It is read-only and collapsible via a button in the page header.
/** False when no conversation/pair is selected at all — distinguishes
* "nothing to show yet" from "selected but empty" (design doc §5).
* Defaults true (existing callers). */
hasSelection?: boolean;
/** True when the messages fetch itself failed — a scoped retry, not the
* page-level OfflineState. */
error?: boolean;
onRetry?:()=>void;
}
exportfunctionA2ATranscript({
messages,
isLoading,
hasSelection=true,
error=false,
onRetry,
}:A2ATranscriptProps)
```
### Entrance Motion
New transcript rows render with `transform`/`opacity` only (no layout thrashing):
- **Initial state:** `opacity-0 scale-y-95 origin-bottom` (transparent, scaled down from bottom)
- **Animation:** One paint frame after the row renders, transitions to `opacity-100 scale-y-100` with `transition-[opacity,transform] duration-200`
- **prefers-reduced-motion guard:** Falls back to instant `opacity-100` and no scale transform
**Implementation:**
The component tracks message IDs via render-phase state (`seenIds`). Newly arrived IDs are flagged in `newRowIds` for one paint frame, then cleared. A `requestAnimationFrame` batches the class application to avoid layout thrashing.
When the user scrolls up and new messages arrive, a dismissable "New messages ↓" pill appears above the transcript, prompting them to scroll to the bottom.
**Behavior:**
- **At bottom:** New rows animate in; pill is hidden
- **Scrolled up:** New rows don't animate in; pill appears instead, allowing the user to catch up at their own pace
- **On dismiss:** Pill vanishes but messages remain in the list
- **On scroll to bottom:** Pill hides and newly arrived rows resume animating in
### Empty & Error States
#### No Conversation Selected
When `hasSelection === false`:
```
┌──────────────────────────────┐
│ │
│ [Messages icon] │
│ "Select a conversation" │
│ │
└──────────────────────────────┘
```
#### Conversation Selected, No Messages
When `hasSelection === true`, `messages.length === 0`, and `error === false`:
```
┌──────────────────────────────┐
│ │
│ [Messages icon] │
│ "No messages yet" │
│ │
└──────────────────────────────┘
```
#### Fetch Error (Scoped Retry)
When `error === true`:
```
┌──────────────────────────────┐
│ │
│ [Alert Triangle icon] │
│ "Failed to load messages" │
│ [Retry button] │
│ │
└──────────────────────────────┘
```
Clicking "Retry" fires the `onRetry()` callback; the consumer is responsible for re-fetching and clearing the `error` flag.
**This is a scoped, transcript-level retry** — not the page-level `OfflineState` that handles network down.
### Message Row Styling
Each message row uses the agent's team color via `TEAM_COLOR_CLASSES`:
- **Avatar:** Colored circle with initials
- **Sender name:** Rendered above the message
- **Timestamp:** Relative time (e.g. "2 minutes ago") from `date-fns`
- **Message body:** Rendered as Markdown via the `<Markdown>` component
## Page Integration
**File:**`panel/src/app/(dashboard)/a2a/page.tsx`
### Grid Layout
The page wires the new components into an `xl:`-responsive grid:
The Stream pane header renders the `A2AConnectionBadge` with the current WebSocket state. When state is `reconnecting` or `disconnected`, the `A2AConnectionBanner` appears above the message list.
**Connection state** is managed via the `useWebSocket()` hook and passed down from page to Stream pane.
Team scoping (Backend, Frontend, etc.) scales to 22 agents without visual noise and doesn't require adding new Tailwind tokens per agent. It maps to the organizational structure and is sufficient for scanning.
### Why `/15` and `/40` Opacity?
The `/15` background and `/40` border provide:
- Visual distinction without overwhelming
- Sufficient contrast for WCAG AA compliance
- Consistency with existing `PairAvatar` pulse treatment
### Why `transform`/`opacity` Only?
Layout-thrashing animations (animating `width`, `height`, `left`, `top`) cause forced reflows on every frame. `transform` and `opacity` changes are GPU-accelerated and don't force layout recalculation, keeping animations smooth.
## Future Enhancements
- Persist context pane width preference (currently only open/closed toggle)
- Add a "Topic" summary in the context pane (when `taskId` is null)
- Extend connection state to show "cached" mode (reading from local storage while reconnecting)