diff --git a/docs/chat-chain-changes/2026-11-05-application-icons.md b/docs/chat-chain-changes/2026-11-05-application-icons.md new file mode 100644 index 0000000..f5f9911 --- /dev/null +++ b/docs/chat-chain-changes/2026-11-05-application-icons.md @@ -0,0 +1,9 @@ +--- +date: 2026-11-05 +pr: pending +feature: Application icon consistency +impact: Chat empty states, session branding and notifications use the application icon. Chat data and events are unchanged. +--- + +The browser icon, application metadata and chat branding use the same icon as +the desktop application, including the legacy favicon and logo assets. diff --git a/packages/client/index.html b/packages/client/index.html index de484d6..42f83e6 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -3,8 +3,8 @@ - - + + diff --git a/packages/client/public/favicon.ico b/packages/client/public/favicon.ico index dbc3f56..f70e966 100644 Binary files a/packages/client/public/favicon.ico and b/packages/client/public/favicon.ico differ diff --git a/packages/client/public/logo.png b/packages/client/public/logo.png index 6a5b1f0..a446a98 100644 Binary files a/packages/client/public/logo.png and b/packages/client/public/logo.png differ diff --git a/packages/client/public/manifest.webmanifest b/packages/client/public/manifest.webmanifest index a018a4f..703296d 100644 --- a/packages/client/public/manifest.webmanifest +++ b/packages/client/public/manifest.webmanifest @@ -9,10 +9,10 @@ "theme_color": "#f7f7f4", "icons": [ { - "src": "/logo.png", - "sizes": "1254x1254", + "src": "/app-logo.png", + "sizes": "512x512", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" } ] } diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue index 4d52107..8419a9a 100644 --- a/packages/client/src/App.vue +++ b/packages/client/src/App.vue @@ -63,7 +63,7 @@ useKeyboard()
diff --git a/packages/client/src/components/hermes/chat/HistoryMessageList.vue b/packages/client/src/components/hermes/chat/HistoryMessageList.vue index 0ca6719..81f5738 100644 --- a/packages/client/src/components/hermes/chat/HistoryMessageList.vue +++ b/packages/client/src/components/hermes/chat/HistoryMessageList.vue @@ -192,7 +192,7 @@ defineExpose({ > diff --git a/packages/client/src/components/hermes/chat/MessageList.vue b/packages/client/src/components/hermes/chat/MessageList.vue index f29028a..4a55d50 100644 --- a/packages/client/src/components/hermes/chat/MessageList.vue +++ b/packages/client/src/components/hermes/chat/MessageList.vue @@ -94,7 +94,7 @@ const visibleToolCalls = computed(() => ); const emptyState = computed(() => ({ - logo: "/logo.png", + logo: "/app-logo.png", alt: "Hermes", text: t("chat.emptyState"), })); diff --git a/packages/client/src/components/hermes/chat/SessionListItem.vue b/packages/client/src/components/hermes/chat/SessionListItem.vue index 02c16b1..14a916a 100644 --- a/packages/client/src/components/hermes/chat/SessionListItem.vue +++ b/packages/client/src/components/hermes/chat/SessionListItem.vue @@ -42,7 +42,7 @@ const profileModelsMissing = computed(() => appStore.profileModelGroups.length > 0 && !profileHasModels.value, ) const isGlobalAgentSession = computed(() => props.session.source === 'global_agent') -const sessionAgentLogo = computed(() => ({ label: 'Hermes', src: '/logo.png' })) +const sessionAgentLogo = computed(() => ({ label: 'Hermes', src: '/app-logo.png' })) let longPressTimer: ReturnType | null = null const longPressTriggered = ref(false) diff --git a/packages/client/src/components/hermes/group-chat/GroupMessageList.vue b/packages/client/src/components/hermes/group-chat/GroupMessageList.vue index 541e288..c08f7a6 100644 --- a/packages/client/src/components/hermes/group-chat/GroupMessageList.vue +++ b/packages/client/src/components/hermes/group-chat/GroupMessageList.vue @@ -105,7 +105,7 @@ defineExpose({ scrollToBottom }) > diff --git a/packages/client/src/stores/hermes/chat.ts b/packages/client/src/stores/hermes/chat.ts index e8f8735..d091a60 100644 --- a/packages/client/src/stores/hermes/chat.ts +++ b/packages/client/src/stores/hermes/chat.ts @@ -2225,7 +2225,7 @@ export const useChatStore = defineStore('chat', () => { } function completionNotificationAgent(_session: Session): { icon: string } { - return { icon: '/favicon.ico' } + return { icon: '/app-logo.png' } } function completionNotificationBody(session: Session, message?: Message): string { diff --git a/tests/client/pwa-metadata.test.ts b/tests/client/pwa-metadata.test.ts index 85ae975..81ff709 100644 --- a/tests/client/pwa-metadata.test.ts +++ b/tests/client/pwa-metadata.test.ts @@ -5,8 +5,9 @@ describe('PWA metadata', () => { it('links manifest and touch icon from the client shell', () => { const html = readFileSync('packages/client/index.html', 'utf8') + expect(html).toContain('rel="icon" type="image/png" href="/app-logo.png"') expect(html).toContain('rel="manifest" href="/manifest.webmanifest"') - expect(html).toContain('rel="apple-touch-icon" href="/logo.png"') + expect(html).toContain('rel="apple-touch-icon" href="/app-logo.png"') expect(html).toContain('name="apple-mobile-web-app-title" content="hermes-ui"') }) @@ -18,10 +19,21 @@ describe('PWA metadata', () => { expect(manifest.start_url).toBe('/#/hermes/chat') expect(manifest.icons).toEqual(expect.arrayContaining([ expect.objectContaining({ - src: '/logo.png', + src: '/app-logo.png', type: 'image/png', - purpose: 'any maskable', + purpose: 'any', }), ])) }) + + it('ships matching application and legacy icon assets', () => { + const logo = readFileSync('packages/client/public/app-logo.png') + const legacyLogo = readFileSync('packages/client/public/logo.png') + const favicon = readFileSync('packages/client/public/favicon.ico') + expect(legacyLogo.equals(logo)).toBe(true) + expect(logo.readUInt32BE(16)).toBe(512) + expect(logo.readUInt32BE(20)).toBe(512) + expect(favicon.readUInt16LE(2)).toBe(1) + expect(favicon.subarray(22, 30)).toEqual(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) + }) }) diff --git a/tests/client/session-list-item.test.ts b/tests/client/session-list-item.test.ts index 8a6b33e..6b5bc6a 100644 --- a/tests/client/session-list-item.test.ts +++ b/tests/client/session-list-item.test.ts @@ -151,7 +151,7 @@ describe('SessionListItem', () => { }) const logo = wrapper.get('.session-item-agent-logo') - expect(logo.attributes('src')).toBe('/logo.png') + expect(logo.attributes('src')).toBe('/app-logo.png') expect(logo.attributes('alt')).toBe('Hermes') expect(wrapper.find('.session-item-agent-name').exists()).toBe(false) }) @@ -172,7 +172,7 @@ describe('SessionListItem', () => { }) const logo = wrapper.get('.session-item-agent-logo') - expect(logo.attributes('src')).toBe('/logo.png') + expect(logo.attributes('src')).toBe('/app-logo.png') expect(logo.attributes('alt')).toBe('Hermes') expect(wrapper.find('.session-item-agent-name').exists()).toBe(false) }) diff --git a/tests/e2e/app-icon.spec.ts b/tests/e2e/app-icon.spec.ts new file mode 100644 index 0000000..89e83ba --- /dev/null +++ b/tests/e2e/app-icon.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from '@playwright/test' + +test('serves the application icon for the browser tab', async ({ page }) => { + await page.goto('/') + const icon = page.locator('link[rel="icon"]') + await expect(icon).toHaveAttribute('href', '/app-logo.png') + await expect(icon).toHaveAttribute('type', 'image/png') + const response = await page.request.get('/app-logo.png') + expect(response.ok()).toBe(true) + expect(response.headers()['content-type']).toContain('image/png') + expect((await response.body()).subarray(0, 8)).toEqual(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) +})