@@ -695,28 +671,6 @@ function handleUpdateClick() {
border-radius: $radius-sm;
}
-.version-management-btn {
- .version-management-label {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- min-width: 0;
- }
-
- .version-update-label {
- display: none;
- flex: 0 0 auto;
- color: inherit;
- font-size: 11px;
- font-weight: 600;
- }
-
- &.has-update .version-update-label {
- display: inline;
- }
-}
-
.changelog-list {
max-height: min(70vh, 640px);
overflow-y: auto;
diff --git a/packages/client/src/components/layout/DesktopTitleBar.vue b/packages/client/src/components/layout/DesktopTitleBar.vue
deleted file mode 100644
index 3822177..0000000
--- a/packages/client/src/components/layout/DesktopTitleBar.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-
-
-
-
-
-
-

-
hermes-ui
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/client/src/components/layout/VersionManagementModal.vue b/packages/client/src/components/layout/VersionManagementModal.vue
deleted file mode 100644
index 0c1a989..0000000
--- a/packages/client/src/components/layout/VersionManagementModal.vue
+++ /dev/null
@@ -1,595 +0,0 @@
-
-
-
-
-
-
-
-
- {{ loadError }}
-
-
- {{ t('runtimeVersions.remoteLoadFailed') }}: {{ status.remoteError }}
-
-
-
-
-
-
{{ t('runtimeVersions.runtimeTitle') }}
-
{{ t('runtimeVersions.platform') }}: {{ status?.platform || '-' }}
-
-
{{ t('runtimeVersions.refresh') }}
-
-
- {{ t('runtimeVersions.activeVersion') }}: {{ status?.hermes.activeVersion || '-' }}
- {{ status?.hermes.activeDirectory || '-' }}
-
-
-
-
- {{ version }}
-
- {{ t('runtimeVersions.active') }}
-
-
- {{ t('runtimeVersions.installed') }}
-
-
- {{ jobLabel(activeJob('runtime', version)!) }}
-
-
-
-
- {{ t('runtimeVersions.useVersion') }}
-
-
-
-
- {{ t('runtimeVersions.deleteVersion') }}
-
-
- {{ t('runtimeVersions.deleteRuntimeConfirm', { version }) }}
-
-
- {{ t('runtimeVersions.downloadGithub') }}
-
-
- {{ t('runtimeVersions.downloadCf') }}
-
-
-
-
{{ t('runtimeVersions.noVersions') }}
-
-
-
-
-
-
-
{{ t('runtimeVersions.webUiTitle') }}
-
{{ t('runtimeVersions.currentWebUi') }}: {{ status?.webui.currentVersion || '-' }}
-
-
-
- {{ t('runtimeVersions.activeVersion') }}: {{ status?.webui.activeVersion || '-' }}
- {{ status?.webui.activeDirectory || '-' }}
-
-
-
-
- {{ version }}
-
- {{ t('runtimeVersions.active') }}
-
-
- {{ t('runtimeVersions.installed') }}
-
-
- {{ jobLabel(activeJob('webui', version)!) }}
-
-
-
-
- {{ t('runtimeVersions.useVersion') }}
-
-
-
-
- {{ t('runtimeVersions.deleteVersion') }}
-
-
- {{ t('runtimeVersions.deleteWebUiConfirm', { version }) }}
-
-
- {{ t('runtimeVersions.downloadGithub') }}
-
-
- {{ t('runtimeVersions.downloadCf') }}
-
-
-
-
{{ t('runtimeVersions.noVersions') }}
-
-
-
-
-
-
{{ t('runtimeVersions.downloadTasks') }}
-
-
-
-
-
{{ job.kind === 'runtime' ? t('runtimeVersions.runtimeTitle') : t('runtimeVersions.webUiTitle') }} {{ job.version }} · {{ sourceLabel(job.source) }}
-
-
- {{ jobProgressText(job) }}
-
-
{{ job.error }}
-
-
{{ jobLabel(job) }}
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts
index cc5e7c8..5bf24b2 100644
--- a/packages/client/src/i18n/locales/en.ts
+++ b/packages/client/src/i18n/locales/en.ts
@@ -18,7 +18,6 @@ export default {
tooManyAttempts: 'Too many failed attempts, please try again later',
lockResetHint: 'If this is your server, clear the login lock with:',
defaultLoginResetHint: 'To reset the default admin password, run:',
- desktopLockResetHint: 'Too many failed login attempts. Open the system tray menu and choose Reset Login to reset the password.',
sessionExpired: 'Login expired. Please sign in again.',
accessDenied: 'You do not have permission to access this resource.',
passwordMismatch: 'Passwords do not match',
@@ -47,6 +46,17 @@ export default {
defaultCredentialLater: 'Remind me later',
},
+ // hermes CLI missing guide
+ hermesMissing: {
+ title: 'Hermes CLI not detected',
+ intro: 'hermes-ui no longer bundles the hermes CLI. Install it on the machine running this service first:',
+ installCmd: 'pip install hermes-agent',
+ envIntro: 'Or point HERMES_BIN at the executable:',
+ envCmd: 'HERMES_BIN=/path/to/hermes',
+ restartHint: 'Restart this service after installing.',
+ retry: 'Re-check',
+ },
+
users: {
title: 'Account Management',
description: 'Create users, assign roles, and control which profiles regular admins can access.',
@@ -236,7 +246,6 @@ export default {
nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.',
changelog: 'Changelog',
noChangelog: 'No changelog available',
- versionManagement: 'Version Management',
updateAvailableLabel: '(New version)',
dockerUpdateTitle: 'Docker Update',
dockerUpdateGuide: 'In Docker environment, please pull the latest image and recreate the container to upgrade:',
@@ -407,7 +416,6 @@ export default {
},
endpoint: {
web: 'Web',
- desktop: 'Desktop',
custom: 'Custom',
},
},
@@ -1216,7 +1224,7 @@ export default {
petdex: {
title: 'Petdex Pets',
- subtitle: 'Browse the public petdex catalog used by Hermes desktop pets.',
+ subtitle: 'Browse the public petdex catalog used by Hermes pets.',
refresh: 'Refresh',
adopt: 'Adopt',
active: 'Active',
@@ -1615,7 +1623,7 @@ export default {
themeDark: 'Dark',
themeSystem: 'System',
chatInputHeight: 'Chat input height',
- chatInputHeightHint: 'Default desktop chat input height in pixels. Mobile keeps auto height.'
+ chatInputHeightHint: 'Default chat input height in pixels on wide screens. Mobile keeps auto height.'
},
agent: {
maxTurns: 'Max Turns',
@@ -2345,7 +2353,7 @@ export default {
// Changelog
changelog: {
- new_0_6_29_9: 'The project is now hermes-ui: the Electron desktop app, marketing site, and ESP32 firmware are removed, leaving only the web client and server',
+ new_0_6_29_9: 'The project is now hermes-ui: the legacy app shell, marketing site, and ESP32 firmware are removed, leaving only the web client and server',
new_0_6_29_10: 'UI branding is unified under hermes-ui, and locales are consolidated to Simplified Chinese and English',
},
}
diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts
index aee993b..129f04f 100644
--- a/packages/client/src/i18n/locales/zh.ts
+++ b/packages/client/src/i18n/locales/zh.ts
@@ -18,7 +18,6 @@ export default {
tooManyAttempts: '登录失败次数过多,请稍后重试',
lockResetHint: '如果这是你的服务器,可以执行以下命令清除登录锁定:',
defaultLoginResetHint: '如需重置默认 admin 密码,可以执行:',
- desktopLockResetHint: '登录失败次数过多。请打开系统托盘菜单,选择“重置登录”来重置密码。',
sessionExpired: '登录已过期,请重新登录',
accessDenied: '你没有权限访问该资源',
passwordMismatch: '两次密码不一致',
@@ -47,6 +46,17 @@ export default {
defaultCredentialLater: '稍后提醒',
},
+ // hermes CLI 缺失引导
+ hermesMissing: {
+ title: '未检测到 hermes CLI',
+ intro: 'hermes-ui 不再内置 hermes CLI,请先在运行本服务的机器上安装:',
+ installCmd: 'pip install hermes-agent',
+ envIntro: '或者设置环境变量指定可执行文件路径:',
+ envCmd: 'HERMES_BIN=/path/to/hermes',
+ restartHint: '安装完成后请重启本服务。',
+ retry: '重新检测',
+ },
+
users: {
title: '账户管理',
description: '创建用户、分配角色,并控制普通管理员可访问的配置。',
@@ -236,7 +246,6 @@ export default {
nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。',
changelog: '更新日志',
noChangelog: '暂无更新日志',
- versionManagement: '版本管理',
updateAvailableLabel: '(有新版)',
dockerUpdateTitle: 'Docker 升级',
dockerUpdateGuide: '当前运行在 Docker 环境中,请拉取最新镜像并重建容器来升级:',
@@ -407,7 +416,6 @@ export default {
},
endpoint: {
web: 'Web 端',
- desktop: '桌面端',
custom: '自定义',
},
},
@@ -2347,7 +2355,7 @@ export default {
// 更新日志
changelog: {
- new_0_6_29_9: '项目更名为 hermes-ui:移除 Electron 桌面端、营销官网与 ESP32 固件,只保留 Web 与服务端',
+ new_0_6_29_9: '项目更名为 hermes-ui:移除桌面端、营销官网与 ESP32 固件,只保留 Web 与服务端',
new_0_6_29_10: '界面品牌文案统一为 hermes-ui,语言包收敛为简体中文与 English 两项',
},
}
diff --git a/packages/client/src/main.ts b/packages/client/src/main.ts
index bb6e724..574b5d2 100644
--- a/packages/client/src/main.ts
+++ b/packages/client/src/main.ts
@@ -5,7 +5,6 @@ import { i18n } from './i18n'
import App from './App.vue'
import './styles/global.scss'
import 'katex/dist/katex.min.css'
-import { desktopBridge } from '@/utils/desktop-bridge'
// Apply theme classes before mount to prevent FOUC (Flash of Unstyled Content)
const savedBrightness = localStorage.getItem('hermes_brightness') || 'system'
@@ -17,9 +16,6 @@ const isDark = savedBrightness === 'dark' || (savedBrightness === 'system' && pr
// Resolve style
const isComic = savedStyle === 'comic'
-const bridge = desktopBridge()
-const isDesktopShell = bridge?.isDesktop === true
-const isDesktopPetWindow = bridge?.windowKind === 'pet' || window.location.hash.startsWith('#/desktop-pet')
// Apply classes to prevent FOUC
if (isDark) {
@@ -28,12 +24,6 @@ if (isDark) {
if (isComic) {
document.documentElement.classList.add('comic')
}
-if (isDesktopShell) {
- document.documentElement.classList.add('hermes-desktop-shell')
-}
-if (isDesktopPetWindow) {
- document.documentElement.classList.add('hermes-desktop-pet-window')
-}
// Read token from URL BEFORE router initializes (hash router strips params)
const urlParams = new URLSearchParams(window.location.search)
diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts
index 864a8a5..3bccc4e 100644
--- a/packages/client/src/router/index.ts
+++ b/packages/client/src/router/index.ts
@@ -1,21 +1,22 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { hasApiKey, isStoredSuperAdmin } from '@/api/client'
+import { checkHermesCliAvailable } from '@/api/hermes/system'
const router = createRouter({
history: createWebHashHistory(),
routes: [
- {
- path: '/desktop-pet',
- name: 'desktop.pet',
- component: () => import('@/views/hermes/DesktopPetView.vue'),
- meta: { public: true },
- },
{
path: '/',
name: 'login',
component: () => import('@/views/LoginView.vue'),
meta: { public: true },
},
+ {
+ path: '/hermes-missing',
+ name: 'hermes.missing',
+ component: () => import('@/views/hermes/HermesMissingView.vue'),
+ meta: { public: true },
+ },
{
path: '/hermes/chat',
name: 'hermes.chat',
@@ -169,29 +170,22 @@ const router = createRouter({
],
})
-async function ensureDesktopAuth(): Promise
{
- if (hasApiKey()) return
- const bridge = (window as typeof window & {
- hermesDesktop?: { isDesktop?: boolean; ensureAuth?: () => Promise }
- }).hermesDesktop
- if (bridge?.isDesktop === true && bridge.ensureAuth) {
- await bridge.ensureAuth().catch(() => false)
- }
-}
-
-function isDesktopShell(): boolean {
- return (window as typeof window & {
- hermesDesktop?: { isDesktop?: boolean }
- }).hermesDesktop?.isDesktop === true
-}
-
router.beforeEach(async (to, _from, next) => {
- await ensureDesktopAuth()
+ // hermes CLI missing guide: probe once per page load before anything else.
+ // checkHermesCliAvailable fails open, so only an explicit "missing" answer
+ // from the server sends the user to the guide page.
+ if (to.name !== 'hermes.missing') {
+ const cliAvailable = await checkHermesCliAvailable()
+ if (!cliAvailable) {
+ next({ name: 'hermes.missing' })
+ return
+ }
+ }
// Public pages don't need auth
if (to.meta.public) {
// Already has key, skip login
- if (to.name === 'login' && hasApiKey() && !isDesktopShell()) {
+ if (to.name === 'login' && hasApiKey()) {
next({ path: '/hermes/chat' })
return
}
diff --git a/packages/client/src/styles/global.scss b/packages/client/src/styles/global.scss
index 5025e02..2c56594 100644
--- a/packages/client/src/styles/global.scss
+++ b/packages/client/src/styles/global.scss
@@ -132,42 +132,6 @@ body {
color: $accent-primary;
}
-.hermes-desktop-pet-window,
-.hermes-desktop-pet-window body,
-.hermes-desktop-pet-window #app {
- background: transparent !important;
- background-color: transparent !important;
-}
-
-// Teleported overlays must remain clickable above Electron drag regions.
-.hermes-desktop-shell {
- .n-drawer,
- .n-drawer *,
- .n-modal,
- .n-modal *,
- .n-dialog,
- .n-dialog *,
- .n-popover,
- .n-popover *,
- .n-dropdown,
- .n-dropdown *,
- .n-select-menu,
- .n-select-menu *,
- .n-base-select-menu,
- .n-base-select-menu *,
- .n-message-container,
- .n-message-container *,
- .n-notification-container,
- .n-notification-container *,
- .drawer-overlay,
- .drawer-panel,
- .drawer-panel * {
- -webkit-app-region: no-drag;
- }
-}
-
-
-
code, pre, .mono {
font-family: $font-code;
}
diff --git a/packages/client/src/utils/completion-notification.ts b/packages/client/src/utils/completion-notification.ts
index df48644..db82998 100644
--- a/packages/client/src/utils/completion-notification.ts
+++ b/packages/client/src/utils/completion-notification.ts
@@ -5,20 +5,11 @@ interface CompletionNotificationPayload {
tag?: string
}
-interface HermesDesktopBridge {
- isDesktop?: boolean
- notifyCompletion?: (payload: CompletionNotificationPayload) => Promise
-}
-
export interface CompletionNotificationPermissionResult {
granted: boolean
reason?: 'unsupported' | 'insecure' | 'denied'
}
-type WindowWithHermesDesktop = Window & typeof globalThis & {
- hermesDesktop?: HermesDesktopBridge
-}
-
function withTimeout(promise: Promise, timeoutMs: number, fallback: T): Promise {
return new Promise((resolve) => {
const timer = window.setTimeout(() => resolve(fallback), timeoutMs)
@@ -35,11 +26,6 @@ function withTimeout(promise: Promise, timeoutMs: number, fallback: T): Pr
})
}
-function desktopBridge(): HermesDesktopBridge | undefined {
- if (typeof window === 'undefined') return undefined
- return (window as WindowWithHermesDesktop).hermesDesktop
-}
-
function supportsBrowserNotification(): boolean {
return typeof window !== 'undefined' && 'Notification' in window
}
@@ -76,12 +62,7 @@ async function showServiceWorkerNotification(payload: CompletionNotificationPayl
}
}
-export function isDesktopNotificationRuntime(): boolean {
- return desktopBridge()?.isDesktop === true
-}
-
export async function requestCompletionNotificationPermission(): Promise {
- if (isDesktopNotificationRuntime()) return { granted: true }
if (!supportsBrowserNotification()) return { granted: false, reason: 'unsupported' }
if (!isBrowserNotificationSecureContext()) return { granted: false, reason: 'insecure' }
if (Notification.permission === 'granted') return { granted: true }
@@ -102,16 +83,6 @@ export async function requestCompletionNotificationPermission(): Promise {
- const bridge = desktopBridge()
- if (bridge?.isDesktop && bridge.notifyCompletion) {
- try {
- return await bridge.notifyCompletion(payload)
- } catch (err) {
- console.warn('Failed to show desktop completion notification:', err)
- return false
- }
- }
-
if (!supportsBrowserNotification() || !isBrowserNotificationSecureContext() || Notification.permission !== 'granted') {
return false
}
diff --git a/packages/client/src/utils/desktop-bridge.ts b/packages/client/src/utils/desktop-bridge.ts
deleted file mode 100644
index b016bba..0000000
--- a/packages/client/src/utils/desktop-bridge.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export interface DesktopWindowBounds {
- x: number
- y: number
- width: number
- height: number
-}
-
-export interface DesktopPetWindowState {
- bounds: DesktopWindowBounds
- visible: boolean
-}
-
-export interface HermesDesktopBridge {
- getToken: () => Promise
- ensureAuth?: () => Promise
- retryBootstrap: (source?: 'cf' | 'github') => Promise
- notifyCompletion: (payload: { title: string; body?: string; icon?: string; tag?: string }) => Promise
- getWindowState: () => Promise<{ isMaximized: boolean }>
- windowControl: (action: 'minimize' | 'toggle-maximize' | 'close') => Promise<{ isMaximized: boolean }>
- getPetWindowState?: () => Promise
- setPetWindowBounds?: (bounds: DesktopWindowBounds) => Promise
- setPetWindowVisible?: (visible: boolean) => Promise
- onPetWindowRefresh?: (callback: () => void) => () => void
- platform: string
- isDesktop: boolean
- windowKind?: 'main' | 'pet'
-}
-
-export type WindowWithHermesDesktop = Window & typeof globalThis & {
- hermesDesktop?: HermesDesktopBridge
-}
-
-export function desktopBridge(): HermesDesktopBridge | undefined {
- return (window as WindowWithHermesDesktop).hermesDesktop
-}
-
-export function isDesktopShell(): boolean {
- return desktopBridge()?.isDesktop === true
-}
-
-export function isDesktopPetWindow(): boolean {
- return desktopBridge()?.windowKind === 'pet'
-}
diff --git a/packages/client/src/views/LoginView.vue b/packages/client/src/views/LoginView.vue
index 9a5d0e7..21963f8 100644
--- a/packages/client/src/views/LoginView.vue
+++ b/packages/client/src/views/LoginView.vue
@@ -2,9 +2,8 @@
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
-import { setApiKey, clearApiKey, hasApiKey } from "@/api/client";
+import { setApiKey, hasApiKey } from "@/api/client";
import { fetchAuthStatus, loginWithPassword } from "@/api/auth";
-import { isDesktopShell } from "@/utils/desktop-bridge";
const { t } = useI18n();
const router = useRouter();
@@ -14,13 +13,8 @@ const password = ref("");
const loading = ref(false);
const errorMsg = ref("");
const showLockResetHint = ref(false);
-const desktopShell = isDesktopShell();
-if (desktopShell) {
- // Desktop login is a recovery path. Drop stale JWTs before any background
- // request can reuse them and show an unrelated expiry notice.
- clearApiKey();
-} else if (hasApiKey()) {
+if (hasApiKey()) {
router.replace("/hermes/chat");
}
@@ -91,15 +85,10 @@ async function handlePasswordLogin() {
{{ errorMsg }}
-
- {{ t("login.desktopLockResetHint") }}
-
-
- {{ t("login.lockResetHint") }}
- hermes-ui clear-login-locks --restart
- {{ t("login.defaultLoginResetHint") }}
- hermes-ui reset-default-login
-
+ {{ t("login.lockResetHint") }}
+ hermes-ui clear-login-locks --restart
+ {{ t("login.defaultLoginResetHint") }}
+ hermes-ui reset-default-login