[a360b6e3] Redesign A2A page with conversation-first layout and agent identity (#401)

* [54b94e44] A2A page: filter controls + agent identity consistency (#387) (#392)

* [54b94e44] feat(a2a): add filter bar and unify agent avatars + pulse across views

Adds a status (active/all) + free-text search filter bar above the A2A
switchboard/list content, backed by a shared a2a-filter-utils module so
both A2ASwitchboard's pairs and A2AConversationList's conversations
narrow identically. Extracts A2APairCard's pulse-flash state into a
reusable usePulseFlash hook and exports its PairAvatar so the classic
conversation list now renders the same two-participant avatar and
emerald pulse-flash affordance the switchboard already had.

* [54b94e44] docs(a2a): add comprehensive filtering and avatar documentation

Documented the new A2A filter bar, filter utilities, pulse-flash hook, and
conversation list API changes. Includes examples, testing guidance, and
migration notes for the pulses prop requirement.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [54417f0c] UX/UI: design A2A conversation-first layout and agent identity (#399)

* [f612a5ab] Add conversation-first layout, agent identity, and live-stream affordance spec (#384)

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [7ed2ef71] docs(ux_ui): add filter-control design spec for A2A conversations (#383)

Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>

* [f563bbc9] Implement conversation-first A2A layout, identity colors, connection states, transcript motion, and empty/error states (#423) (#427)

* [f563bbc9] feat(a2a): conversation-first layout, team-color identity, connection states, transcript motion, empty/error states

Implements docs/ux_ui/design/02-conversation-first-layout-agent-identity-live-stream.md:
- xl:+ collapsible Context pane (identity cards, linked-task summary, no-task hint), persisted via the existing zustand ui-store
- getAgentTeamColor + TEAM_COLOR_CLASSES in agent-utils.ts, applied to PairAvatar, the transcript row avatar, and the context pane
- A2AConnectionBadge/A2AConnectionBanner rendering all four ConnectionState values distinctly with a motion-reduce-guarded pulsing dot and a dismissable reconnecting/disconnected strip
- A2ATranscript: transform/opacity-only new-row entrance transition, scrolled-up "New messages" pill, split hasSelection/empty/error states with a scoped Retry
- Unit tests for every new pure helper and component

* [f563bbc9] docs(a2a): conversation-first layout, team-color identity, connection states, transcript motion, empty/error states

Document the new conversation-first A2A layout features:
- Agent team-color system (getAgentTeamColor, TEAM_COLOR_CLASSES) for six cell buckets
- A2AContextPane component with identity cards, linked task summary, no-task hint
- Connection state rendering (A2AConnectionBadge, A2AConnectionBanner) for all four ConnectionState values
- Transcript entrance motion with transform/opacity-only transitions and prefers-reduced-motion guards
- Split empty/error states (no selection, no messages, fetch error with scoped retry)
- Page-level integration with xl:+ responsive grid layout

Includes component API, usage examples, testing guidance, accessibility notes, and design rationale.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [478f027c] Implement A2A conversations filter control per conversations-filter-control.md (#445) (#448)

* [478f027c] feat(a2a): add multi-dimension Popover filter panel for A2A conversations

Replace the free-text search + active/all toggle with the Popover-triggered
filter control from conversations-filter-control.md: Agent multi-select
checkboxes, a Task id-fragment input with a "No linked task" toggle, Status
toggle buttons, and a date range, plus an active-filter chip row and Clear
all. Filtering applies to both the switchboard (Agent only) and conversation
list (all four dimensions) per the design doc's per-view rules.

* [478f027c] docs(a2a): add comprehensive filter-control guide covering component API, filter dimensions, and per-view rules

Documents A2AFilterBar component and filter utilities with:
- Component API and props
- All 4 filter dimensions (Agent, Task, Status, Date range)
- Per-view rules (Switchboard vs List)
- Usage examples and parent setup
- Filter logic and match predicates
- Testing guide and accessibility notes
- Design notes on client-side filtering limitation
- Links to related components and the design spec

Helps developers understand, use, and maintain the A2A conversations
filter control without needing to read the design doc or component source.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-11 07:46:29 +02:00
committed by GitHub
co-authored by Frontend Developer 1 Frontend Documenter UX/UI Developer 1 UX/UI Developer 2 Frontend Developer 2 Renn F
parent 58354a364e
commit 6e57066bd6
33 changed files with 3999 additions and 552 deletions
+303
View File
@@ -0,0 +1,303 @@
# A2A Page Filtering & Agent Identity
## Overview
The A2A (Agent-to-Agent) page now features a unified filter bar and consistent agent identity rendering across both the switchboard (org-chart view) and classic conversation list. Both views respond identically to filtering and pulse animations, creating a cohesive experience regardless of the active view.
## Filter Bar
The `A2AFilterBar` component renders above the switchboard/list content and provides two independent filtering controls:
### Component
**File:** `panel/src/components/a2a/a2a-filter-bar.tsx`
**Props:**
- `status` (`A2AStatusFilter`): Current status filter, either `"active"` or `"all"`
- `onStatusChange` (callback): Fires when the Active/All toggle changes
- `search` (string): Current search query
- `onSearchChange` (callback): Fires as the user types in the search input
**Rendering:**
- Search input with placeholder "Search agent or topic..." accepts free-text queries
- Two toggle buttons: "Active" narrows to conversations with live activity; "All" shows everything
- Compact styling (7px button height, 3px font size) to avoid crowding the view
**Usage:**
```tsx
import { A2AFilterBar } from "@/components/a2a/a2a-filter-bar";
<A2AFilterBar
status={statusFilter}
onStatusChange={setStatusFilter}
search={search}
onSearchChange={setSearch}
/>
```
## Filter Utilities
The `a2a-filter-utils.ts` module exports pure, testable filtering logic shared by both views.
**File:** `panel/src/components/a2a/a2a-filter-utils.ts`
### `A2AStatusFilter` Type
```typescript
type A2AStatusFilter = "active" | "all";
```
### `filterConversations()`
Narrows the conversation list to the matching subset based on status and search query.
**Parameters:**
- `conversations`: `ReadonlyArray<AdminConversationSummary>`
- `status`: `A2AStatusFilter``"active"` filters to `conversation.status === "active"`; `"all"` passes all
- `search`: `string` — free-text query (case-insensitive)
**Behavior:**
- Searches across both agent slugs (raw IDs), their display names (via `getAgentDisplayName`), and the topic
- Empty search passes all conversations
- Status filter is applied first, then search
**Example:**
```typescript
const filtered = filterConversations(
conversations,
"active",
"backend qa"
);
// Returns only active conversations where one agent is Backend QA or mentions "backend qa"
```
### `filterPairs()`
Narrows the switchboard pairs to the matching subset based on status and search query.
**Parameters:**
- `pairs`: `ReadonlyArray<AdminPairSummary>`
- `status`: `A2AStatusFilter``"active"` filters to pairs that have a `conversation_id` (have A2A'd); `"all"` passes all
- `search`: `string` — free-text query (case-insensitive)
**Behavior:**
- Searches across both agent slugs (raw IDs) and their display names
- Empty search passes all pairs
- Status filter is applied first, then search
- Note: `AdminPairSummary` has no backend `status` field, so "active" is interpreted as "has a conversation"
**Example:**
```typescript
const filtered = filterPairs(pairs, "active", "auditor");
// Returns only pairs where at least one agent matches "auditor" and the pair has an active conversation
```
## Conversation List Updates
The conversation list now accepts a `pulses` prop and renders agent avatars.
**File:** `panel/src/components/a2a/a2a-conversation-list.tsx`
### `A2AConversationListProps`
**New prop:**
- `pulses` (`Record<string, number>`): Maps `pairKey(agent_a, agent_b)` to the epoch timestamp of the latest pulse frame. This is the same `pulses` map the switchboard uses, keyed identically, so a conversation row flashes in sync with its pair's card on the switchboard.
**Example:**
```tsx
<A2AConversationList
conversations={filteredConversations}
selectedId={selectedId}
onSelect={handleSelect}
isLoading={loadingConversations}
pulses={pulses} // New: from page state
/>
```
### `ConversationRow` Subcomponent
Each conversation renders as a `ConversationRow` that mirrors the switchboard's pair card styling:
- **Avatars:** Two `PairAvatar` components (initials, colors) matching `A2APairCard`
- **Pulse animation:** Uses `usePulseFlash()` to determine if the row should flash hot
- **Styling:** Emerald background + shadow while pulsing, matches switchboard
- **Selection state:** Bordered/highlighted when selected, same as switchboard selection
### Breaking Change
The `pulses` prop is **required**. If you're calling `A2AConversationList` from outside the A2A page, you must supply it:
```typescript
// Old code (will TypeScript error)
<A2AConversationList conversations={data} ... />
// New code
const pulses = { "be-dev-1|be-qa": 1700000000000 };
<A2AConversationList conversations={data} ... pulses={pulses} />
```
If you don't have a pulses map available, pass an empty object `{}` — rows won't flash, but selection/interaction will work normally.
## Pulse-Flash Hook
**File:** `panel/src/hooks/use-pulse-flash.ts`
The `usePulseFlash()` hook extracts the pulse-flash animation logic from inline state in `A2APairCard`, making it reusable across the switchboard and conversation list.
### `usePulseFlash(pulsedAt: number | null): boolean`
Returns `true` for one paint frame after `pulsedAt` changes to a non-null value, then `false`. The consumer's CSS `transition-duration` does the actual fade-out.
**How it works:**
1. Seeded to `null` (not the initial `pulsedAt`), so a component that *mounts* already carrying a live pulse still flashes hot
2. On render, if `pulsedAt !== lastSeenPulse`, updates `lastSeenPulse` and sets `isPulsing = true` if `pulsedAt !== null`
3. On the next paint frame (via `requestAnimationFrame`), flips `isPulsing` back to `false`
4. CSS transition handles the fade — `transition-duration: PAIR_PULSE_FADE_MS` applied to elements that conditionally render the hot styling
**Example:**
```tsx
import { usePulseFlash } from "@/hooks/use-pulse-flash";
import { PAIR_PULSE_FADE_MS } from "@/components/a2a/a2a-switchboard-utils";
export function MyPulsedRow({ pulsedAt }: { pulsedAt: number | null }) {
const isPulsing = usePulseFlash(pulsedAt);
return (
<div
className={cn("p-2", isPulsing && "bg-emerald-500/15")}
style={{ transitionDuration: `${PAIR_PULSE_FADE_MS}ms` }}
>
{/* content */}
</div>
);
}
```
## A2A Page Integration
**File:** `panel/src/app/(dashboard)/a2a/page.tsx`
The page composes these pieces:
1. **Filter state:** Lifts `statusFilter` and `search` to page level
2. **Derived state:** Computes `filteredPairs` and `filteredConversations` via `useMemo` on each render
3. **Filter bar:** Mounts `A2AFilterBar` above the view content
4. **Synchronized pulses:** Both `A2ASwitchboard` and `A2AConversationList` receive the same `pulses` map, keyed identically, so pulse animations sync across views
**Code sketch:**
```tsx
const [statusFilter, setStatusFilter] = useState<A2AStatusFilter>("all");
const [search, setSearch] = useState("");
const filteredPairs = useMemo(
() => filterPairs(pairs, statusFilter, search),
[pairs, statusFilter, search]
);
const filteredConversations = useMemo(
() => filterConversations(conversations, statusFilter, search),
[conversations, statusFilter, search]
);
// Both views receive the same pulses map
<A2AFilterBar
status={statusFilter}
onStatusChange={setStatusFilter}
search={search}
onSearchChange={setSearch}
/>
{view === "switchboard" ? (
<A2ASwitchboard pairs={filteredPairs} pulses={pulses} ... />
) : (
<A2AConversationList conversations={filteredConversations} pulses={pulses} ... />
)}
```
## Testing
All filtering and pulse behavior is covered by tests:
- **`a2a-filter-utils.test.ts`:** `filterConversations()` and `filterPairs()` with status/search combinations
- **`a2a-filter-bar.test.tsx`:** Button pressed state, search input changes, status toggle callbacks
- **`a2a-conversation-list.test.tsx`:** Avatar rendering, pulse flash detection (tests the `data-pulsing` attribute)
- **`page.test.tsx`:** Filter bar renders, narrows switchboard independently, narrows classic list independently
Run tests with:
```bash
cd panel
pnpm test
```
## Design Notes
### Status Filter Semantics
The "active" filter has different semantics across views due to data availability:
- **Conversations:** `active` filters to `conversation.status === "active"` (backend-provided status)
- **Pairs:** `active` filters to pairs with a non-null `conversation_id` (have A2A'd at least once)
This is intentional and documented in code comments. If the backend adds an explicit `AdminPairSummary.status` field in the future, update `filterPairs()` to use it instead of the `conversation_id` heuristic.
### Pulse Animation Timing
The pulse flash is a brief, high-contrast alert (emerald bg + shadow) that fades over `PAIR_PULSE_FADE_MS` (200ms by default, defined in `a2a-switchboard-utils.ts`). If you're experiencing chop or seeing the animation cut off, check:
1. The `usePulseFlash()` hook is being called (not bypassed)
2. The consumer element has `transition-[background-color,box-shadow]` or equivalent CSS
3. The `transitionDuration` inline style matches the fade constant
## Migration Guide
### If you use `A2AConversationList` in another context:
**Before:**
```tsx
<A2AConversationList
conversations={conversations}
selectedId={selectedId}
onSelect={handleSelect}
isLoading={false}
/>
```
**After:**
```tsx
<A2AConversationList
conversations={conversations}
selectedId={selectedId}
onSelect={handleSelect}
isLoading={false}
pulses={{}} // Add this prop (empty object if no pulses available)
/>
```
### If you extract pulse-flash logic into other components:
Import and use the `usePulseFlash()` hook instead of rolling your own state management:
```tsx
import { usePulseFlash } from "@/hooks/use-pulse-flash";
import { PAIR_PULSE_FADE_MS } from "@/components/a2a/a2a-switchboard-utils";
const isPulsing = usePulseFlash(pulsedAt);
// Now use isPulsing to conditionally render the hot styling
```