fix: use app icon across browser and chat
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-11-05 12:01:20 +08:00
parent 34233f0ee0
commit c50a6f63d9
14 changed files with 49 additions and 16 deletions
@@ -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.
+2 -2
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo.png" />
<link rel="icon" type="image/png" href="/app-logo.png" />
<link rel="apple-touch-icon" href="/app-logo.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#1a1d23" />
<meta name="apple-mobile-web-app-capable" content="yes" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 224 KiB

+3 -3
View File
@@ -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"
}
]
}
+1 -1
View File
@@ -63,7 +63,7 @@ useKeyboard()
</div>
<div class="app-layout" :class="{ 'no-sidebar': isLoginPage || !showAppSidebar }">
<button v-if="showMobileMenuButton" class="hamburger-btn" @click="handleMobileMenuClick">
<img src="/logo.png" alt="Menu" style="width: 24px; height: 24px;" />
<img src="/app-logo.png" alt="Menu" style="width: 24px; height: 24px;" />
</button>
<div v-if="!isLoginPage && showAppSidebar && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
<AppSidebar v-if="!isLoginPage && showAppSidebar" />
@@ -192,7 +192,7 @@ defineExpose({
>
<template #empty>
<div class="empty-state">
<img :src="'/coding-agents/hermes.png'" alt="Hermes" class="empty-logo" />
<img :src="'/app-logo.png'" alt="Hermes" class="empty-logo" />
<p>{{ t("chat.emptyState") }}</p>
</div>
</template>
@@ -94,7 +94,7 @@ const visibleToolCalls = computed(() =>
);
const emptyState = computed(() => ({
logo: "/logo.png",
logo: "/app-logo.png",
alt: "Hermes",
text: t("chat.emptyState"),
}));
@@ -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<typeof setTimeout> | null = null
const longPressTriggered = ref(false)
@@ -105,7 +105,7 @@ defineExpose({ scrollToBottom })
>
<template #empty>
<div class="empty-state">
<img :src="'/coding-agents/hermes.png'" alt="Hermes" class="empty-logo" />
<img :src="'/app-logo.png'" alt="Hermes" class="empty-logo" />
<p>{{ t("chat.emptyState") }}</p>
</div>
</template>
+1 -1
View File
@@ -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 {
+15 -3
View File
@@ -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]))
})
})
+2 -2
View File
@@ -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)
})
+12
View File
@@ -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]))
})