Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
date: 2026-07-29
pr: 2259
feature: Desktop Group Chat Agent list
impact: Group Chat Agent avatars can open their Agent list from draggable macOS and Windows desktop headers.
---

The Agent avatar stack is now a semantic button and explicitly opts out of the
Electron drag region, preserving the draggable header around it while restoring
pointer and keyboard interaction.
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,11 @@ async function handleApproval(choice: 'once' | 'session' | 'always' | 'deny') {
<!-- Stacked avatars (user + agents) -->
<NPopover v-if="store.agents.length" trigger="click" placement="bottom-end" :width="220">
<template #trigger>
<div class="avatar-stack-inner">
<button
type="button"
class="avatar-stack-inner avatar-stack-trigger"
:aria-label="`${t('groupChat.agents')} (${store.agents.length})`"
>
<!-- User avatar first -->
<span class="avatar-stack-item" :style="{ zIndex: store.agents.length + 1 }">
<ProfileAvatar class="agent-avatar" :name="store.userName || store.userId" :avatar="userMemberAvatar" :size="24" />
Expand All @@ -818,7 +822,7 @@ async function handleApproval(choice: 'once' | 'session' | 'always' | 'deny') {
<ProfileAvatar class="agent-avatar" :name="agentAvatarName(agent)" :avatar="profileAvatarFor(agent.profile)" :size="24" />
</span>
<span v-if="store.agents.length > 4" class="avatar-stack-more">+{{ store.agents.length - 4 }}</span>
</div>
</button>
</template>
<div class="agent-popover">
<div class="agent-popover-item" style="margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--n-border-color, #efeff5);">
Expand Down Expand Up @@ -2081,6 +2085,15 @@ export default defineComponent({ components: { CreateRoomForm } })
align-items: center;
}

.avatar-stack-trigger {
padding: 0;
border: 0;
color: inherit;
background: transparent;
cursor: pointer;
-webkit-app-region: no-drag;
}

.avatar-stack-item {
width: 28px;
height: 28px;
Expand Down
53 changes: 50 additions & 3 deletions tests/e2e/group-chat-room-deeplink.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { expect, test, type Page, type Route } from '@playwright/test'
import { authenticate, TEST_MODEL_GROUP } from './fixtures'

type DesktopPlatform = 'darwin' | 'win32'

const baseRooms = [
{ id: 'room-alpha', name: 'Alpha Room', inviteCode: 'ALPHA1', canManage: true, workspace: '/tmp/alpha', triggerTokens: 100000, maxHistoryTokens: 32000, tailMessageCount: 10, totalTokens: 123 },
{ id: 'room-beta', name: 'Beta Room', inviteCode: 'BETA22', canManage: true, workspace: '/tmp/beta', triggerTokens: 100000, maxHistoryTokens: 32000, tailMessageCount: 10, totalTokens: 456 },
Expand Down Expand Up @@ -39,6 +41,20 @@ const messagesByRoom: Record<string, unknown[]> = {
],
}

const agentsByRoom: Record<string, unknown[]> = {
'room-alpha': [
{
id: 'agent-row-1',
roomId: 'room-alpha',
agentId: 'agent-1',
profile: 'default',
name: 'Worker',
description: 'Group agent',
invited: 1,
},
],
}

async function mockGroupChatApi(page: Page) {
const rooms = baseRooms.map(room => ({ ...room }))
const inviteCodeUpdates: Array<{ roomId: string, body: unknown }> = []
Expand Down Expand Up @@ -105,7 +121,7 @@ async function mockGroupChatApi(page: Page) {
const roomId = decodeURIComponent(detailMatch[1])
const room = rooms.find(r => r.id === roomId)
return room
? json({ room, messages: messagesByRoom[roomId] || [], agents: [], members: [{ id: 'member-1', userId: 'user-1', name: 'User One', description: '', joinedAt: 1_790_000_000 }] })
? json({ room, messages: messagesByRoom[roomId] || [], agents: agentsByRoom[roomId] || [], members: [{ id: 'member-1', userId: 'user-1', name: 'User One', description: '', joinedAt: 1_790_000_000 }] })
: json({ error: 'Room not found' }, 404)
}

Expand All @@ -123,6 +139,7 @@ async function mockGroupChatSocket(page: Page) {
body: `
const state = window.__PW_GROUP_SOCKET__ || (window.__PW_GROUP_SOCKET__ = { sockets: [], emitted: [] })
const roomMessages = ${JSON.stringify(messagesByRoom)}
const roomAgents = ${JSON.stringify(agentsByRoom)}
function makeSocket(url, options) {
const listeners = new Map()
const socket = {
Expand All @@ -139,7 +156,7 @@ function makeSocket(url, options) {
state.emitted.push({ event, payload })
if (event === 'join' && typeof ack === 'function') {
const roomId = payload && payload.roomId
setTimeout(() => ack({ roomId, roomName: roomId, members: [], messages: roomMessages[roomId] || [], agents: [], rooms: [], typingUsers: [], contextStatuses: [] }), 0)
setTimeout(() => ack({ roomId, roomName: roomId, members: [], messages: roomMessages[roomId] || [], agents: roomAgents[roomId] || [], rooms: [], typingUsers: [], contextStatuses: [] }), 0)
}
if (event === 'message' && typeof ack === 'function') {
setTimeout(() => ack({ id: payload && payload.id }), 0)
Expand Down Expand Up @@ -171,7 +188,22 @@ export default { io }
})
}

async function setup(page: Page, path: string) {
async function installDesktopBridge(page: Page, platform: DesktopPlatform) {
await page.addInitScript((desktopPlatform) => {
Object.defineProperty(window, 'hermesDesktop', {
configurable: true,
value: {
isDesktop: true,
platform: desktopPlatform,
getWindowState: async () => ({ isMaximized: false }),
windowControl: async () => ({ isMaximized: false }),
},
})
}, platform)
}

async function setup(page: Page, path: string, platform?: DesktopPlatform) {
if (platform) await installDesktopBridge(page, platform)
await authenticate(page)
await mockGroupChatSocket(page)
const api = await mockGroupChatApi(page)
Expand Down Expand Up @@ -222,6 +254,21 @@ test.describe('group chat room deep links', () => {
await expect(page.locator('.group-workspace-panel')).toHaveCount(0)
})

for (const platform of ['darwin', 'win32'] as const) {
test(`opens the Agent list from the ${platform} desktop drag header`, async ({ page }) => {
await setup(page, '/#/hermes/group-chat/room/room-alpha', platform)

const trigger = page.getByRole('button', { name: 'Agents (1)' })
await expect(trigger).toBeVisible()
await expect(trigger).toHaveCSS('-webkit-app-region', 'no-drag')
await trigger.click()

const popover = page.locator('.n-popover .agent-popover')
await expect(popover).toBeVisible()
await expect(popover.locator('.agent-popover-name', { hasText: 'Worker' })).toBeVisible()
})
}

test('room settings rotate invite codes only after the update API succeeds', async ({ page }) => {
const api = await setup(page, '/#/hermes/group-chat/room/room-alpha')

Expand Down
Loading