Skip to content

Commit f137e6e

Browse files
wtj-0527Lux
andauthored
fix(group-chat): glow the complete room avatar (#2701)
* fix group room avatar glow shape * docs link room avatar glow PR --------- Co-authored-by: Lux <wangw9475@agent.qq.com>
1 parent c8d8cc3 commit f137e6e

5 files changed

Lines changed: 80 additions & 42 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
date: 2026-08-23
3+
pr: 2701
4+
feature: Group room avatar active-glow geometry
5+
impact: Running rooms glow the complete rounded-square composite avatar while in-conversation running Agent avatars retain the matching circular rainbow treatment.
6+
---
7+
8+
## Goal
9+
10+
Treat the persistent Group Chat room-list composite as one session avatar while
11+
retaining the established active-Agent rainbow language inside the room.
12+
13+
## Visual contract
14+
15+
- Any running visible or hidden Agent activates one outer glow around the full
16+
36px room avatar.
17+
- The room-list glow follows a rounded-square silhouette.
18+
- Individual 2x2 member cells and the `+N` overflow cell do not glow.
19+
- In-room running Agent avatars remain circular.
20+
- Both shapes use the existing Direct Chat / Group Chat four-second rainbow
21+
palette and reduced-motion fallback.
22+
23+
## Non-goals
24+
25+
- No activity identity, roster, avatar, API, persistence, permission, or
26+
runtime-protocol changes.

packages/client/src/components/hermes/group-chat/GroupRoomAgentAvatar.vue

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const avatarSize = computed(() => {
2727
if (visibleCount.value === 2) return 17
2828
return 15
2929
})
30-
const overflowActive = computed(() => (
31-
hiddenAgents.value.some(agent => activeAgentIds.value.has(agent.id))
30+
const hasActiveAgent = computed(() => (
31+
props.agents.some(agent => activeAgentIds.value.has(agent.id))
3232
))
3333
const rosterNames = computed(() => props.agents.map(agent => agent.name).join(', '))
3434
const runningNames = computed(() => (
@@ -58,10 +58,12 @@ const accessibleSummary = computed(() => {
5858
<template>
5959
<div
6060
class="room-agent-grid"
61+
:class="{ 'is-active': hasActiveAgent }"
6162
:data-agent-count="visibleCount"
6263
role="img"
6364
:aria-label="accessibleSummary"
6465
:title="accessibleSummary"
66+
:aria-busy="hasActiveAgent"
6567
>
6668
<span
6769
v-if="agents.length === 0"
@@ -78,11 +80,9 @@ const accessibleSummary = computed(() => {
7880
v-else
7981
:key="agent.id"
8082
class="room-agent-grid-cell agent"
81-
:class="{ 'is-active': activeAgentIds.has(agent.id) }"
8283
:data-agent-id="agent.id"
8384
:title="agent.name"
8485
:aria-label="agent.name"
85-
:aria-busy="activeAgentIds.has(agent.id)"
8686
>
8787
<ProfileAvatar
8888
:name="agent.agent || agent.name"
@@ -93,10 +93,8 @@ const accessibleSummary = computed(() => {
9393
<span
9494
v-if="hiddenAgents.length"
9595
class="room-agent-grid-cell room-agent-grid-overflow"
96-
:class="{ 'is-active': overflowActive }"
9796
:title="hiddenAgents.map(agent => agent.name).join(', ')"
9897
:aria-label="hiddenAgents.map(agent => agent.name).join(', ')"
99-
:aria-busy="overflowActive"
10098
>
10199
+{{ hiddenAgents.length }}
102100
</span>
@@ -117,6 +115,20 @@ const accessibleSummary = computed(() => {
117115
border: 1px solid $border-color;
118116
border-radius: 8px;
119117
background: $bg-secondary;
118+
119+
&.is-active::after {
120+
position: absolute;
121+
z-index: 2;
122+
inset: -4px;
123+
border-radius: 12px;
124+
box-shadow:
125+
0 0 0 2px #ff6b6b,
126+
0 0 10px rgba(255, 107, 107, 0.4),
127+
0 0 20px rgba(255, 107, 107, 0.2);
128+
content: '';
129+
animation: room-avatar-rainbow-glow 4s linear infinite;
130+
pointer-events: none;
131+
}
120132
}
121133
122134
.room-agent-grid-cell {
@@ -132,19 +144,6 @@ const accessibleSummary = computed(() => {
132144
border-radius: 4px;
133145
}
134146
135-
&.is-active::after {
136-
position: absolute;
137-
z-index: 2;
138-
inset: -4px;
139-
border-radius: 8px;
140-
box-shadow:
141-
0 0 0 2px #ff6b6b,
142-
0 0 10px rgba(255, 107, 107, 0.4),
143-
0 0 20px rgba(255, 107, 107, 0.2);
144-
content: '';
145-
animation: room-agent-rainbow-glow 4s linear infinite;
146-
pointer-events: none;
147-
}
148147
}
149148
150149
.room-agent-grid[data-agent-count='0'] .room-agent-grid-cell,
@@ -241,7 +240,7 @@ const accessibleSummary = computed(() => {
241240
}
242241
}
243242
244-
@keyframes room-agent-rainbow-glow {
243+
@keyframes room-avatar-rainbow-glow {
245244
0% {
246245
box-shadow:
247246
0 0 0 2px #ff6b6b,
@@ -293,7 +292,7 @@ const accessibleSummary = computed(() => {
293292
}
294293
295294
@media (prefers-reduced-motion: reduce) {
296-
.room-agent-grid-cell.is-active::after {
295+
.room-agent-grid.is-active::after {
297296
animation: none;
298297
}
299298
}

tests/client/group-chat-panel-workspace-source.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,13 @@ describe('GroupChatPanel workspace save handling', () => {
396396
expect(panel).toContain(':active-agent-ids="store.activeAgentIdsForRoom(room.id)"')
397397
expect(roomAvatar).toContain('data-agent-count')
398398
expect(roomAvatar).toContain('room-agent-grid-neutral')
399-
expect(roomAvatar).toContain('animation: room-agent-rainbow-glow 4s linear infinite')
400-
expect(roomAvatar).toContain('@keyframes room-agent-rainbow-glow')
399+
expect(roomAvatar).toContain(`:class="{ 'is-active': hasActiveAgent }"`)
400+
expect(roomAvatar).toContain(':aria-busy="hasActiveAgent"')
401+
expect(roomAvatar).not.toContain(`:class="{ 'is-active': activeAgentIds.has(agent.id) }"`)
402+
expect(roomAvatar).not.toContain(`:class="{ 'is-active': overflowActive }"`)
403+
expect(roomAvatar).toContain('animation: room-avatar-rainbow-glow 4s linear infinite')
404+
expect(roomAvatar).toContain('@keyframes room-avatar-rainbow-glow')
405+
expect(roomAvatar).toMatch(/\.room-agent-grid[\s\S]*?&\.is-active::after\s*\{[\s\S]*?border-radius: 12px;/)
401406
expect(roomAvatar).toContain('0 0 0 2px #ff6b6b')
402407
expect(roomAvatar).toContain('0 0 0 2px #48dbfb')
403408
expect(roomAvatar).toContain('0 0 0 2px #5f27cd')

tests/client/group-room-agent-avatar.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('GroupRoomAgentAvatar', () => {
9595
)
9696
})
9797

98-
it('activates only the matching persistent Agent cell and deduplicates parallel runs', () => {
98+
it('activates the complete room avatar when a visible Agent is running', () => {
9999
const wrapper = mount(GroupRoomAgentAvatar, {
100100
props: {
101101
agents: [agent(1), agent(2), agent(3)],
@@ -104,16 +104,16 @@ describe('GroupRoomAgentAvatar', () => {
104104
},
105105
})
106106

107-
expect(wrapper.findAll('.room-agent-grid-cell.is-active')).toHaveLength(1)
108-
expect(wrapper.get('[data-agent-id="row-2"]').classes()).toContain('is-active')
109-
expect(wrapper.get('[data-agent-id="row-2"]').attributes('aria-busy')).toBe('true')
110-
expect(wrapper.get('[data-agent-id="row-1"]').attributes('aria-busy')).toBe('false')
107+
expect(wrapper.get('.room-agent-grid').classes()).toContain('is-active')
108+
expect(wrapper.get('.room-agent-grid').attributes('aria-busy')).toBe('true')
109+
expect(wrapper.findAll('.room-agent-grid-cell.is-active')).toHaveLength(0)
110+
expect(wrapper.get('[data-agent-id="row-2"]').attributes('aria-busy')).toBeUndefined()
111111
expect(wrapper.get('.room-agent-grid').attributes('aria-label')).toBe(
112112
'Room agents. Room Agents: Agent 1, Agent 2, Agent 3. Running Agents: Agent 2.',
113113
)
114114
})
115115

116-
it('marks the overflow cell when only a hidden Agent is running', () => {
116+
it('activates the complete room avatar when only a hidden Agent is running', () => {
117117
const wrapper = mount(GroupRoomAgentAvatar, {
118118
props: {
119119
agents: Array.from({ length: 6 }, (_, index) => agent(index + 1)),
@@ -123,8 +123,10 @@ describe('GroupRoomAgentAvatar', () => {
123123
})
124124

125125
expect(wrapper.get('.room-agent-grid-overflow').text()).toBe('+3')
126-
expect(wrapper.get('.room-agent-grid-overflow').classes()).toContain('is-active')
127-
expect(wrapper.findAll('.room-agent-grid-cell.agent.is-active')).toHaveLength(0)
126+
expect(wrapper.get('.room-agent-grid').classes()).toContain('is-active')
127+
expect(wrapper.get('.room-agent-grid').attributes('aria-busy')).toBe('true')
128+
expect(wrapper.get('.room-agent-grid-overflow').classes()).not.toContain('is-active')
129+
expect(wrapper.findAll('.room-agent-grid-cell.is-active')).toHaveLength(0)
128130
expect(wrapper.get('.room-agent-grid').attributes('aria-label')).toBe(
129131
'Room agents. Room Agents: Agent 1, Agent 2, Agent 3, Agent 4, Agent 5, Agent 6. Running Agents: Agent 6.',
130132
)
@@ -142,6 +144,7 @@ describe('GroupRoomAgentAvatar', () => {
142144

143145
await wrapper.setProps({ activeAgentIds: ['row-1'] })
144146

147+
expect(wrapper.get('.room-agent-grid').classes()).toContain('is-active')
145148
expect(wrapper.get('.room-agent-grid').attributes('aria-label')).not.toBe(idleLabel)
146149
expect(wrapper.get('.room-agent-grid').attributes('aria-label')).toContain('Running Agents: Agent 1.')
147150
})

tests/e2e/group-chat-room-deeplink.spec.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ test.describe('group chat room deep links', () => {
648648
expect(toolNames.at(-1)).toBe('live_tool_1')
649649
})
650650

651-
test('keeps persistent room Agent grids stable while exact runs activate only matching cells', async ({ page }) => {
651+
test('keeps persistent room Agent grids stable while active runs glow the complete room avatar', async ({ page }) => {
652652
await setup(page, '/#/hermes/group-chat/room/room-alpha')
653653

654654
const activity = (overrides: Record<string, unknown> = {}) => ({
@@ -673,6 +673,7 @@ test.describe('group chat room deep links', () => {
673673
await expect(alphaGrid).toHaveAttribute('data-agent-count', '1')
674674
await expect(betaGrid).toHaveAttribute('data-agent-count', '1')
675675
await expect(emptyGrid).toHaveAttribute('data-agent-count', '0')
676+
await expect(alphaGrid).not.toHaveClass(/is-active/)
676677
await expect(alphaGrid.locator('.room-agent-grid-cell.is-active')).toHaveCount(0)
677678
await expect(emptyGrid.locator('.room-agent-grid-neutral')).toHaveCount(1)
678679
await expect(alphaGrid).toHaveCSS('width', '36px')
@@ -685,16 +686,18 @@ test.describe('group chat room deep links', () => {
685686
await triggerGroupSocket(page, 'room_agent_activity', activity({ runId: 'run-live-tools-2' }))
686687
const activeAlphaAgent = alphaGrid.locator('[data-agent-id="agent-row-1"]')
687688
const activeRunAvatar = page.locator('.group-agent-run[data-run-id="run-live-tools"] .run-avatar')
688-
await expect(activeAlphaAgent).toHaveClass(/is-active/)
689-
await expect(alphaGrid.locator('.room-agent-grid-cell.is-active')).toHaveCount(1)
689+
await expect(alphaGrid).toHaveClass(/is-active/)
690+
await expect(alphaGrid).toHaveAttribute('aria-busy', 'true')
691+
await expect(activeAlphaAgent).not.toHaveClass(/is-active/)
692+
await expect(alphaGrid.locator('.room-agent-grid-cell.is-active')).toHaveCount(0)
690693
await expect(activeRunAvatar).toHaveClass(/run-avatar-active/)
691694
await expect(page.locator('.group-agent-run[data-run-id="run-history-tools"] .run-avatar')).not.toHaveClass(/run-avatar-active/)
692695
if (evidenceDir) {
693696
await alphaRoom.screenshot({ path: `${evidenceDir}/active-room-rainbow.png` })
694697
await activeRunAvatar.scrollIntoViewIfNeeded()
695698
await page.screenshot({ path: `${evidenceDir}/active-message-rainbow.png` })
696699
}
697-
await expect.poll(() => activeAlphaAgent.evaluate((element) => {
700+
await expect.poll(() => alphaGrid.evaluate((element) => {
698701
const glow = getComputedStyle(element, '::after')
699702
return {
700703
inset: glow.top,
@@ -704,8 +707,8 @@ test.describe('group chat room deep links', () => {
704707
}
705708
})).toMatchObject({
706709
inset: '-4px',
707-
animation: expect.stringContaining('room-agent-rainbow-glow'),
708-
radius: '8px',
710+
animation: expect.stringContaining('room-avatar-rainbow-glow'),
711+
radius: '12px',
709712
shadow: expect.not.stringMatching(/^none$/),
710713
})
711714
await expect.poll(() => activeRunAvatar.evaluate((element) => {
@@ -727,7 +730,7 @@ test.describe('group chat room deep links', () => {
727730
})
728731
await page.emulateMedia({ reducedMotion: 'reduce' })
729732
await expect.poll(() => Promise.all([
730-
activeAlphaAgent.evaluate(element => getComputedStyle(element, '::after').animationName),
733+
alphaGrid.evaluate(element => getComputedStyle(element, '::after').animationName),
731734
activeRunAvatar.evaluate(element => getComputedStyle(element, '::before').animationName),
732735
activeRunAvatar.evaluate(element => getComputedStyle(element, '::before').boxShadow),
733736
])).toEqual(['none', 'none', expect.stringContaining('rgb(255, 107, 107)')])
@@ -742,14 +745,15 @@ test.describe('group chat room deep links', () => {
742745

743746
await expect(alphaRoom.locator(':scope > .room-icon')).toHaveCount(0)
744747
await expect(alphaRoom.locator(':scope > .room-agent-grid + .room-info')).toHaveCount(1)
745-
await expect(betaGrid.locator('[data-agent-id="agent-row-runtime"]')).toHaveClass(/is-active/)
748+
await expect(betaGrid).toHaveClass(/is-active/)
749+
await expect(betaGrid.locator('[data-agent-id="agent-row-runtime"]')).not.toHaveClass(/is-active/)
746750
await expect.poll(() => alphaRoom.locator('.room-info').evaluate(element => element.getBoundingClientRect().x)).toBe(alphaInfoX)
747751
await expect.poll(() => betaRoom.locator('.room-info').evaluate(element => element.getBoundingClientRect().x)).toBe(betaInfoX)
748752

749753
await triggerGroupSocket(page, 'room_agent_activity', activity({ status: 'ready' }))
750-
await expect(alphaGrid.locator('[data-agent-id="agent-row-1"]')).toHaveClass(/is-active/)
754+
await expect(alphaGrid).toHaveClass(/is-active/)
751755
await triggerGroupSocket(page, 'room_agent_activity', activity({ runId: 'run-live-tools-2', status: 'ready' }))
752-
await expect(alphaGrid.locator('.room-agent-grid-cell.is-active')).toHaveCount(0)
756+
await expect(alphaGrid).not.toHaveClass(/is-active/)
753757

754758
await triggerGroupSocket(page, 'agents_updated', {
755759
roomId: 'room-alpha',
@@ -779,7 +783,8 @@ test.describe('group chat room deep links', () => {
779783
await betaRoom.click()
780784
await expect(page).toHaveURL(/#\/hermes\/group-chat\/room\/room-beta$/)
781785
await expect(alphaGrid).toHaveAttribute('data-agent-count', '2')
782-
await expect(betaGrid.locator('[data-agent-id="agent-row-runtime"]')).toHaveClass(/is-active/)
786+
await expect(betaGrid).toHaveClass(/is-active/)
787+
await expect(betaGrid.locator('[data-agent-id="agent-row-runtime"]')).not.toHaveClass(/is-active/)
783788
})
784789

785790
test('removes an interrupted run approval from the room approval surface', async ({ page }) => {

0 commit comments

Comments
 (0)