Skip to content
Closed
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
36 changes: 32 additions & 4 deletions apps/desktop/src/plugins/hermes-bots/bot-row.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ import type * as HermesSdk from '@hermes/plugin-sdk'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { BotRow } from './bot-row'
import { BotRow, GroupRow } from './bot-row'
import { $groupChats } from './group-chat'
import { translateBots } from './i18n-test-helper'
import type { RosterRow } from './types'
import { translateBotsIn } from './i18n-test-helper'
import type { GroupMember, RosterRow } from './types'

// Which shipped bundle the rendered rows resolve their strings against. `en`
// matches the literals this file once carried, so a case that has to prove a
// string comes from the catalog reads the same row under `ja`.
const locale = vi.hoisted(() => ({ current: 'en' as 'en' | 'ja' }))

const { ensureAgent, ensureBotMetadata, notifyError, openRosterBot, requestProfile, warmAgent, warmProfile } =
vi.hoisted(() => ({
Expand All @@ -43,7 +48,7 @@ vi.mock('@hermes/plugin-sdk', async importOriginal => {
host: { ...sdk.host, ensureAgent, notifyError, requestProfile, warmAgent, warmProfile },
// The plugin bundle normally lands via `ctx.i18n.register` at load, so
// without this every localized label in the row renders empty.
usePluginI18n: () => translateBots
usePluginI18n: () => translateBotsIn(locale.current)
}
})

Expand Down Expand Up @@ -199,3 +204,26 @@ describe('context-menu mutations hydrate the alias first', () => {
expect(params).toMatchObject({ name: 'backend-worker', ui_meta: { 'hermes-bots': { pinned: false } } })
})
})

describe('a group row', () => {
const members = [{ name: 'alpha' }, { name: 'beta' }, { name: 'gamma' }] as GroupMember[]
const row = <GroupRow active={false} group="crew" members={members} needsYou={false} onDisband={noop} onOpen={noop} />

beforeEach(() => {
locale.current = 'en'
})

it('previews an empty room and describes it to assistive tech in the active language', () => {
const english = render(row)

expect(english.getByText('3 bots')).toBeTruthy()
expect(english.getByRole('button', { name: 'crew, 3 bots, 3 of 3 available' })).toBeTruthy()
english.unmount()

locale.current = 'ja'
const japanese = render(row)

expect(japanese.getByText('ボット3体')).toBeTruthy()
expect(japanese.getByRole('button', { name: 'crew, ボット3体, 3体中3体が利用可能' })).toBeTruthy()
})
})
6 changes: 3 additions & 3 deletions apps/desktop/src/plugins/hermes-bots/bot-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,14 @@ export function GroupRow({ active, group, members, needsYou, onOpen, onDisband }

const preview = last
? `${last.from?.kind === 'user' ? 'You' : `@${lastHandle}`}: ${stripPreviewMarkdown(last.text) || '…'}`
: `${members.length} bots`
: b.group.memberCount(members.length)

const availableMembers = members.filter(member => botSourceStatus(member).available).length
const availabilityLabel = `${availableMembers} of ${members.length} available`
const availabilityLabel = b.group.availableCount(availableMembers, members.length)

const row = (
<RowButton
aria-label={`${group}, ${members.length} bots, ${availabilityLabel}`}
aria-label={`${group}, ${b.group.memberCount(members.length)}, ${availabilityLabel}`}
className={cn(
'flex w-full min-w-0 max-w-full items-center gap-2.5 overflow-hidden rounded-md px-2 py-2 text-left transition-colors',
'hover:bg-(--chrome-action-hover)',
Expand Down
28 changes: 17 additions & 11 deletions apps/desktop/src/plugins/hermes-bots/i18n-test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@

import { BOTS_LOCALES } from './i18n'

export function translateBots(key: string, ...args: unknown[]): string {
const value = key
.split('.')
.reduce<unknown>(
(node, part) => (node && typeof node === 'object' ? (node as Record<string, unknown>)[part] : undefined),
BOTS_LOCALES.en
)
/** The same resolver against another shipped locale, for a test that has to
* tell a catalog string from an English literal that happens to match `en`. */
export function translateBotsIn(locale: keyof typeof BOTS_LOCALES) {
return (key: string, ...args: unknown[]): string => {
const value = key
.split('.')
.reduce<unknown>(
(node, part) => (node && typeof node === 'object' ? (node as Record<string, unknown>)[part] : undefined),
BOTS_LOCALES[locale]
)

if (typeof value === 'function') {
return String((value as (...params: unknown[]) => string)(...args))
}
if (typeof value === 'function') {
return String((value as (...params: unknown[]) => string)(...args))
}

return typeof value === 'string' ? value : key
return typeof value === 'string' ? value : key
}
}

export const translateBots = translateBotsIn('en')
6 changes: 6 additions & 0 deletions apps/desktop/src/plugins/hermes-bots/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ type BotsMessages = {
pictureGenerationFailed: string
nameTaken: (name: string) => string
memberCount: (count: number) => string
/** How many of a room's members are reachable right now. */
availableCount: (available: number, total: number) => string
settingsHint: (group: string) => string
settingsLabel: (group: string) => string
disbandHint: (group: string) => string
Expand Down Expand Up @@ -411,6 +413,7 @@ const en: BotsMessages = {
pictureGenerationFailed: 'Group picture generation failed',
nameTaken: name => `A group named “${name}” already exists.`,
memberCount: count => `${count} bots`,
availableCount: (available, total) => `${available} of ${total} available`,
settingsHint: group => `Group settings — rename ${group} or set a room picture`,
settingsLabel: group => `Group settings for ${group}`,
disbandHint: group => `Disband the ${group} group chat`,
Expand Down Expand Up @@ -630,6 +633,7 @@ const ja: BotsMessages = {
pictureGenerationFailed: 'グループ画像の生成に失敗しました',
nameTaken: name => `「${name}」という名前のグループはすでに存在します。`,
memberCount: count => `ボット${count}体`,
availableCount: (available, total) => `${total}体中${available}体が利用可能`,
settingsHint: group => `グループ設定 — ${group}の名前変更やルーム画像の設定`,
settingsLabel: group => `${group}のグループ設定`,
disbandHint: group => `${group}グループチャットを解散`,
Expand Down Expand Up @@ -844,6 +848,7 @@ const zh: BotsMessages = {
pictureGenerationFailed: '群组图片生成失败',
nameTaken: name => `已存在名为“${name}”的群聊。`,
memberCount: count => `${count} 个机器人`,
availableCount: (available, total) => `${total} 个中 ${available} 个可用`,
settingsHint: group => `群聊设置 — 重命名 ${group} 或设置房间图片`,
settingsLabel: group => `${group} 的群聊设置`,
disbandHint: group => `解散 ${group} 群聊`,
Expand Down Expand Up @@ -1058,6 +1063,7 @@ const zhHant: BotsMessages = {
pictureGenerationFailed: '群組圖片產生失敗',
nameTaken: name => `已存在名為「${name}」的群組聊天。`,
memberCount: count => `${count} 個機器人`,
availableCount: (available, total) => `${total} 個中 ${available} 個可用`,
settingsHint: group => `群組設定 — 重新命名 ${group} 或設定房間圖片`,
settingsLabel: group => `${group} 的群組設定`,
disbandHint: group => `解散 ${group} 群組聊天`,
Expand Down