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({
>
-
+
{{ t("chat.emptyState") }}
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 })
>
-
+
{{ t("chat.emptyState") }}
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]))
+})