fix: use app icon across browser and chat
This commit is contained in:
@@ -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.
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/png" href="/app-logo.png" />
|
||||||
<link rel="apple-touch-icon" href="/logo.png" />
|
<link rel="apple-touch-icon" href="/app-logo.png" />
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<meta name="theme-color" content="#1a1d23" />
|
<meta name="theme-color" content="#1a1d23" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<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 |
@@ -9,10 +9,10 @@
|
|||||||
"theme_color": "#f7f7f4",
|
"theme_color": "#f7f7f4",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/logo.png",
|
"src": "/app-logo.png",
|
||||||
"sizes": "1254x1254",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ useKeyboard()
|
|||||||
</div>
|
</div>
|
||||||
<div class="app-layout" :class="{ 'no-sidebar': isLoginPage || !showAppSidebar }">
|
<div class="app-layout" :class="{ 'no-sidebar': isLoginPage || !showAppSidebar }">
|
||||||
<button v-if="showMobileMenuButton" class="hamburger-btn" @click="handleMobileMenuClick">
|
<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>
|
</button>
|
||||||
<div v-if="!isLoginPage && showAppSidebar && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
|
<div v-if="!isLoginPage && showAppSidebar && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
|
||||||
<AppSidebar v-if="!isLoginPage && showAppSidebar" />
|
<AppSidebar v-if="!isLoginPage && showAppSidebar" />
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ defineExpose({
|
|||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty-state">
|
<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>
|
<p>{{ t("chat.emptyState") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const visibleToolCalls = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const emptyState = computed(() => ({
|
const emptyState = computed(() => ({
|
||||||
logo: "/logo.png",
|
logo: "/app-logo.png",
|
||||||
alt: "Hermes",
|
alt: "Hermes",
|
||||||
text: t("chat.emptyState"),
|
text: t("chat.emptyState"),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const profileModelsMissing = computed(() =>
|
|||||||
appStore.profileModelGroups.length > 0 && !profileHasModels.value,
|
appStore.profileModelGroups.length > 0 && !profileHasModels.value,
|
||||||
)
|
)
|
||||||
const isGlobalAgentSession = computed(() => props.session.source === 'global_agent')
|
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
|
let longPressTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const longPressTriggered = ref(false)
|
const longPressTriggered = ref(false)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ defineExpose({ scrollToBottom })
|
|||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty-state">
|
<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>
|
<p>{{ t("chat.emptyState") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2225,7 +2225,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completionNotificationAgent(_session: Session): { icon: string } {
|
function completionNotificationAgent(_session: Session): { icon: string } {
|
||||||
return { icon: '/favicon.ico' }
|
return { icon: '/app-logo.png' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function completionNotificationBody(session: Session, message?: Message): string {
|
function completionNotificationBody(session: Session, message?: Message): string {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ describe('PWA metadata', () => {
|
|||||||
it('links manifest and touch icon from the client shell', () => {
|
it('links manifest and touch icon from the client shell', () => {
|
||||||
const html = readFileSync('packages/client/index.html', 'utf8')
|
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="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"')
|
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.start_url).toBe('/#/hermes/chat')
|
||||||
expect(manifest.icons).toEqual(expect.arrayContaining([
|
expect(manifest.icons).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
src: '/logo.png',
|
src: '/app-logo.png',
|
||||||
type: 'image/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]))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ describe('SessionListItem', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const logo = wrapper.get('.session-item-agent-logo')
|
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(logo.attributes('alt')).toBe('Hermes')
|
||||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -172,7 +172,7 @@ describe('SessionListItem', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const logo = wrapper.get('.session-item-agent-logo')
|
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(logo.attributes('alt')).toBe('Hermes')
|
||||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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]))
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user