diff --git a/packages/client/src/components/layout/AppSidebar.vue b/packages/client/src/components/layout/AppSidebar.vue
index 48aafaf..041e72f 100644
--- a/packages/client/src/components/layout/AppSidebar.vue
+++ b/packages/client/src/components/layout/AppSidebar.vue
@@ -43,6 +43,10 @@ async function handleUpdate() {
}
}
+function handleReloadClient() {
+ appStore.reloadClient();
+}
+
function handleLogout() {
localStorage.clear();
router.replace({ name: 'login' });
@@ -296,6 +300,9 @@ function openChangelog() {
Web UI v{{ appStore.serverVersion || "0.1.0" }}
+
+ {{ t('sidebar.reloadClientVersion', { version: appStore.serverVersion }) }}
+
{{ appStore.updating ? t('sidebar.updating') : t('sidebar.updateVersion', { version: appStore.latestVersion }) }}
diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts
index a4ad438..779993d 100644
--- a/packages/client/src/i18n/locales/de.ts
+++ b/packages/client/src/i18n/locales/de.ts
@@ -89,6 +89,7 @@ export default {
disconnected: 'Getrennt',
updateTip: 'Fuhren Sie "hermes-web-ui update" im Terminal aus, um zu aktualisieren',
updateVersion: 'Aktualisieren auf v{version}',
+ reloadClientVersion: 'Für v{version} neu laden',
updating: 'Aktualisierung...',
updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten',
updateFailed: 'Aktualisierung fehlgeschlagen',
diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts
index 67b26e8..cf59dee 100644
--- a/packages/client/src/i18n/locales/en.ts
+++ b/packages/client/src/i18n/locales/en.ts
@@ -101,6 +101,7 @@ export default {
expand: 'Expand menu',
updateTip: 'Run "hermes-web-ui update" in terminal to update',
updateVersion: 'Upgrade to v{version}',
+ reloadClientVersion: 'Reload for v{version}',
updating: 'Updating...',
updateSuccess: 'Update complete, please restart the server',
updateFailed: 'Update failed',
diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts
index e62011b..801d2db 100644
--- a/packages/client/src/i18n/locales/es.ts
+++ b/packages/client/src/i18n/locales/es.ts
@@ -89,6 +89,7 @@ export default {
disconnected: 'Desconectado',
updateTip: 'Ejecuta "hermes-web-ui update" en la terminal para actualizar',
updateVersion: 'Actualizar a v{version}',
+ reloadClientVersion: 'Recargar para v{version}',
updating: 'Actualizando...',
updateSuccess: 'Actualizacion completa, por favor reinicia el servidor',
updateFailed: 'Error al actualizar',
diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts
index 2c5927b..be9c571 100644
--- a/packages/client/src/i18n/locales/fr.ts
+++ b/packages/client/src/i18n/locales/fr.ts
@@ -89,6 +89,7 @@ export default {
disconnected: 'Deconnecte',
updateTip: 'Executez "hermes-web-ui update" dans le terminal pour mettre a jour',
updateVersion: 'Mettre a jour vers v{version}',
+ reloadClientVersion: 'Recharger pour v{version}',
updating: 'Mise a jour...',
updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur',
updateFailed: 'Echec de la mise a jour',
diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts
index 92f8bd8..0f85513 100644
--- a/packages/client/src/i18n/locales/ja.ts
+++ b/packages/client/src/i18n/locales/ja.ts
@@ -89,6 +89,7 @@ export default {
disconnected: '未接続',
updateTip: 'ターミナルで "hermes-web-ui update" を実行して更新してください',
updateVersion: 'v{version} にアップグレード',
+ reloadClientVersion: 'v{version} に再読み込み',
updating: '更新中...',
updateSuccess: '更新が完了しました。サーバーを再起動してください',
updateFailed: '更新に失敗しました',
diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts
index e891823..613f58e 100644
--- a/packages/client/src/i18n/locales/ko.ts
+++ b/packages/client/src/i18n/locales/ko.ts
@@ -89,6 +89,7 @@ export default {
disconnected: '연결 끊김',
updateTip: '터미널에서 "hermes-web-ui update"를 실행하여 업데이트하세요',
updateVersion: 'v{version}(으)로 업그레이드',
+ reloadClientVersion: 'v{version}(으)로 새로고침',
updating: '업데이트 중...',
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
updateFailed: '업데이트 실패',
diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts
index 4e32d7a..e53c502 100644
--- a/packages/client/src/i18n/locales/pt.ts
+++ b/packages/client/src/i18n/locales/pt.ts
@@ -89,6 +89,7 @@ export default {
disconnected: 'Desconectado',
updateTip: 'Execute "hermes-web-ui update" no terminal para atualizar',
updateVersion: 'Atualizar para v{version}',
+ reloadClientVersion: 'Recarregar para v{version}',
updating: 'Atualizando...',
updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor',
updateFailed: 'Falha na atualizacao',
diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts
index d5c48f4..08af624 100644
--- a/packages/client/src/i18n/locales/zh-TW.ts
+++ b/packages/client/src/i18n/locales/zh-TW.ts
@@ -101,6 +101,7 @@ export default {
expand: '展開選單',
updateTip: '在終端機執行 "hermes-web-ui update" 即可更新',
updateVersion: '升級版本 v{version}',
+ reloadClientVersion: '重新整理到 v{version}',
updating: '正在更新...',
updateSuccess: '更新完成,請重新啟動服務',
updateFailed: '更新失敗',
diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts
index 4c32bf7..a992ef5 100644
--- a/packages/client/src/i18n/locales/zh.ts
+++ b/packages/client/src/i18n/locales/zh.ts
@@ -101,6 +101,7 @@ export default {
expand: '展开菜单',
updateTip: '在终端运行 "hermes-web-ui update" 即可更新',
updateVersion: '升级版本 v{version}',
+ reloadClientVersion: '刷新到 v{version}',
updating: '正在更新...',
updateSuccess: '更新完成,请重启服务',
updateFailed: '更新失败',
diff --git a/packages/client/src/stores/hermes/app.ts b/packages/client/src/stores/hermes/app.ts
index 38c3395..a4bd511 100644
--- a/packages/client/src/stores/hermes/app.ts
+++ b/packages/client/src/stores/hermes/app.ts
@@ -26,6 +26,7 @@ export const useAppStore = defineStore('app', () => {
const serverVersion = ref(WEB_UI_VERSION)
const latestVersion = ref('')
const updateAvailable = ref(false)
+ const clientOutdated = ref(false)
const updating = ref(false)
const modelGroups = ref([])
const selectedModel = ref('')
@@ -63,11 +64,13 @@ export const useAppStore = defineStore('app', () => {
const res = await checkHealth()
connected.value = res.status === 'ok'
if (res.webui_version) serverVersion.value = res.webui_version
+ clientOutdated.value = !!res.webui_version && res.webui_version !== WEB_UI_VERSION
if (res.webui_latest) latestVersion.value = res.webui_latest
updateAvailable.value = !!res.webui_update_available
if (res.node_version) nodeVersion.value = res.node_version
} catch {
connected.value = false
+ clientOutdated.value = false
}
}
@@ -231,6 +234,12 @@ export const useAppStore = defineStore('app', () => {
}
}
+ function reloadClient() {
+ const url = new URL(window.location.href)
+ url.searchParams.set('__hwui_reload', Date.now().toString())
+ window.location.replace(url.toString())
+ }
+
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
@@ -259,8 +268,10 @@ export const useAppStore = defineStore('app', () => {
latestVersion,
nodeVersion,
updateAvailable,
+ clientOutdated,
updating,
doUpdate,
+ reloadClient,
modelGroups,
customModels,
modelAliases,
diff --git a/tests/client/app-store.test.ts b/tests/client/app-store.test.ts
index fc43f9f..0dcd78a 100644
--- a/tests/client/app-store.test.ts
+++ b/tests/client/app-store.test.ts
@@ -152,6 +152,38 @@ describe('App Store', () => {
expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()
})
+ it('marks the client stale when the served Web UI version changes', async () => {
+ mockSystemApi.checkHealth.mockResolvedValue({
+ status: 'ok',
+ webui_version: '0.5.17',
+ webui_latest: '0.5.17',
+ webui_update_available: false,
+ })
+ const store = useAppStore()
+
+ await store.checkConnection()
+
+ expect(store.connected).toBe(true)
+ expect(store.serverVersion).toBe('0.5.17')
+ expect(store.clientOutdated).toBe(true)
+ expect(store.updateAvailable).toBe(false)
+ })
+
+ it('does not mark the client stale when the served Web UI version matches this bundle', async () => {
+ mockSystemApi.checkHealth.mockResolvedValue({
+ status: 'ok',
+ webui_version: 'test',
+ webui_latest: 'test',
+ webui_update_available: false,
+ })
+ const store = useAppStore()
+
+ await store.checkConnection()
+
+ expect(store.serverVersion).toBe('test')
+ expect(store.clientOutdated).toBe(false)
+ })
+
it('clears the updating state and reports failure when self-update request fails', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))
diff --git a/tests/client/sidebar-search.test.ts b/tests/client/sidebar-search.test.ts
index 0e3a55e..6a12fe1 100644
--- a/tests/client/sidebar-search.test.ts
+++ b/tests/client/sidebar-search.test.ts
@@ -3,6 +3,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
const openSessionSearchMock = vi.hoisted(() => vi.fn())
+const mockAppStore = vi.hoisted(() => ({
+ sidebarOpen: true,
+ sidebarCollapsed: false,
+ connected: true,
+ serverVersion: 'test',
+ latestVersion: '',
+ updateAvailable: false,
+ clientOutdated: false,
+ updating: false,
+ toggleSidebar: vi.fn(),
+ toggleSidebarCollapsed: vi.fn(),
+ closeSidebar: vi.fn(),
+ doUpdate: vi.fn(),
+ reloadClient: vi.fn(),
+}))
vi.mock('@/composables/useSessionSearch', () => ({
useSessionSearch: () => ({
@@ -11,16 +26,7 @@ vi.mock('@/composables/useSessionSearch', () => ({
}))
vi.mock('@/stores/hermes/app', () => ({
- useAppStore: () => ({
- sidebarOpen: true,
- connected: true,
- serverVersion: 'test',
- updateAvailable: false,
- updating: false,
- toggleSidebar: vi.fn(),
- closeSidebar: vi.fn(),
- doUpdate: vi.fn(),
- }),
+ useAppStore: () => mockAppStore,
}))
vi.mock('vue-router', async (importOriginal) => {
@@ -55,7 +61,7 @@ vi.mock('naive-ui', async () => {
error: vi.fn(),
}),
NButton: {
- template: '',
+ template: '',
},
NSelect: {
template: '',
@@ -68,6 +74,12 @@ import AppSidebar from '@/components/layout/AppSidebar.vue'
describe('AppSidebar search entry', () => {
beforeEach(() => {
openSessionSearchMock.mockClear()
+ mockAppStore.serverVersion = 'test'
+ mockAppStore.latestVersion = ''
+ mockAppStore.updateAvailable = false
+ mockAppStore.clientOutdated = false
+ mockAppStore.updating = false
+ mockAppStore.reloadClient.mockClear()
})
it('opens the session search modal from the sidebar button', async () => {
@@ -90,4 +102,26 @@ describe('AppSidebar search entry', () => {
await searchButton!.trigger('click')
expect(openSessionSearchMock).toHaveBeenCalledTimes(1)
})
+
+ it('offers a client reload when the server version differs from the loaded bundle', async () => {
+ mockAppStore.clientOutdated = true
+ mockAppStore.serverVersion = '0.5.17'
+ const wrapper = mount(AppSidebar, {
+ global: {
+ stubs: {
+ ProfileSelector: true,
+ ModelSelector: true,
+ LanguageSwitch: true,
+ ThemeSwitch: true,
+ },
+ },
+ })
+
+ const reloadButton = wrapper.findAll('button')
+ .find(node => node.text().includes('sidebar.reloadClientVersion'))
+ expect(reloadButton).toBeTruthy()
+
+ await reloadButton!.trigger('click')
+ expect(mockAppStore.reloadClient).toHaveBeenCalledTimes(1)
+ })
})