From 4701bf0ad07a5153c1929d45e0029d7d3c92d17b Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 16 Oct 2026 15:33:56 +0800 Subject: [PATCH] Phase 004: remove the desktop shell, website tooling, and bundled hermes CLI - Drop all desktop/Electron branches, scripts, and UI (title bar, pets, desktop bridge); LAN endpoint kind is now web/custom only - Replace the bundled hermes CLI with user-side installation: add GET /api/hermes/cli-status plus a HermesMissingView onboarding guide - Remove the version-management UI (snapshots handled by git) and prune dead website/mcu/claude scripts and lockfile entries --- .../2026-10-16-phase004-remove-desktop.md | 37 + docs/openapi.json | 26 + package.json | 17 +- packages/client/src/App.vue | 24 +- packages/client/src/api/client.ts | 17 - packages/client/src/api/hermes/devices.ts | 2 +- packages/client/src/api/hermes/system.ts | 29 + .../auth/DefaultCredentialPrompt.vue | 9 - .../src/components/hermes/pets/WebPet.vue | 708 ------------------ .../src/components/layout/AppSidebar.vue | 46 -- .../src/components/layout/DesktopTitleBar.vue | 171 ----- .../layout/VersionManagementModal.vue | 595 --------------- packages/client/src/i18n/locales/en.ts | 20 +- packages/client/src/i18n/locales/zh.ts | 16 +- packages/client/src/main.ts | 10 - packages/client/src/router/index.ts | 42 +- packages/client/src/styles/global.scss | 36 - .../src/utils/completion-notification.ts | 29 - packages/client/src/utils/desktop-bridge.ts | 43 -- packages/client/src/views/LoginView.vue | 23 +- .../src/views/hermes/DesktopPetView.vue | 32 - .../client/src/views/hermes/DevicesView.vue | 4 +- .../src/views/hermes/HermesMissingView.vue | 121 +++ .../client/src/views/hermes/PetdexView.vue | 7 - packages/server/src/config.ts | 2 +- packages/server/src/controllers/auth.ts | 4 +- packages/server/src/controllers/devices.ts | 4 +- packages/server/src/controllers/health.ts | 20 +- .../server/src/db/hermes/devices-store.ts | 2 +- packages/server/src/index.ts | 104 +-- packages/server/src/routes/health.ts | 2 + .../services/hermes/agent-bridge/manager.ts | 4 - .../src/services/hermes/gateway-autostart.ts | 165 +--- .../src/services/hermes/hermes-process.ts | 21 +- .../services/hermes/studio-mcp-autoinject.ts | 13 +- packages/server/src/services/lan-discovery.ts | 7 +- .../src/services/runtime-version-manager.ts | 22 +- packages/server/src/services/shutdown.ts | 4 +- packages/skills/apikey-image-gen/SKILL.md | 1 - packages/skills/grok-image-to-video/SKILL.md | 1 - tests/client/app-web-pet.test.ts | 126 ---- tests/client/desktop-titlebar.test.ts | 45 -- tests/client/login-view.test.ts | 33 - tests/client/router-login-redirect.test.ts | 22 +- tests/e2e/fixtures.ts | 5 + tests/server/gateway-autostart.test.ts | 70 -- tests/server/lan-discovery.test.ts | 2 +- tests/server/shutdown.test.ts | 10 +- tests/server/studio-mcp-autoinject.test.ts | 4 +- 49 files changed, 354 insertions(+), 2403 deletions(-) create mode 100644 docs/chat-chain-changes/2026-10-16-phase004-remove-desktop.md delete mode 100644 packages/client/src/components/hermes/pets/WebPet.vue delete mode 100644 packages/client/src/components/layout/DesktopTitleBar.vue delete mode 100644 packages/client/src/components/layout/VersionManagementModal.vue delete mode 100644 packages/client/src/utils/desktop-bridge.ts delete mode 100644 packages/client/src/views/hermes/DesktopPetView.vue create mode 100644 packages/client/src/views/hermes/HermesMissingView.vue delete mode 100644 tests/client/app-web-pet.test.ts delete mode 100644 tests/client/desktop-titlebar.test.ts diff --git a/docs/chat-chain-changes/2026-10-16-phase004-remove-desktop.md b/docs/chat-chain-changes/2026-10-16-phase004-remove-desktop.md new file mode 100644 index 0000000..1f133a1 --- /dev/null +++ b/docs/chat-chain-changes/2026-10-16-phase004-remove-desktop.md @@ -0,0 +1,37 @@ +--- +date: 2026-10-16 +pr: pending +feature: Phase 004 - remove the desktop shell and bundled hermes CLI +impact: No chat-chain behavior change; the bridge manager only loses an unused HERMES_DESKTOP helper, and gateway autostart no longer branches on desktop mode. +--- + +# Phase 004 — Remove the desktop shell + +Phase: `docs/phases/phase_004.md` + +## What Changed + +Phase 004 removes Electron packaging, the version-management UI, and the +bundled hermes CLI. Two chat-chain-adjacent files were touched: + +- `packages/server/src/services/hermes/agent-bridge/manager.ts` — deleted the + unused `isDesktopRuntime()` helper (it read `HERMES_DESKTOP`; no call site + remained after earlier cleanup). Gateway IPC readiness, spawn, and kill paths + are unchanged. +- `packages/server/src/services/hermes/gateway-autostart.ts` — removed the + Windows desktop orphan-recovery branch (`shouldRecoverWindowsDesktopGatewayOrphans` + / `recoverWindowsDesktopGatewayOrphans`) and its `HERMES_DESKTOP` gating. + Managed gateway recovery on other platforms keeps its existing behavior. + +## Contract Check + +`/chat-run` event names, field names, field types, and emission timing are +unchanged. No run dispatch, session, or usage-recording behavior changed. + +## Verification + +```bash +npm run harness:check +npm run build +npm run test +``` diff --git a/docs/openapi.json b/docs/openapi.json index 99a40a4..cf901c4 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2675,6 +2675,32 @@ } } }, + "/api/hermes/cli-status": { + "get": { + "tags": [ + "Health" + ], + "summary": "Get cli-status", + "description": "GET /api/hermes/cli-status", + "operationId": "hermesCliStatus", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "description": "Not found" + } + } + } + }, "/api/hermes/config": { "get": { "tags": [ diff --git a/package.json b/package.json index d9d374d..471e3bc 100644 --- a/package.json +++ b/package.json @@ -45,22 +45,7 @@ "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "dev:website": "vite --config vite.config.website.ts", - "build:website": "vite build --config vite.config.website.ts", - "preview:website": "vite preview --config vite.config.website.ts", - "mcu:v1:flash:clean": "pio run -d packages/esp32-c3/v1 && pio run -d packages/esp32-c3/v1 -t erase && pio run -d packages/esp32-c3/v1 -t upload", - "mcu:v2:flash:clean": "pio run -d packages/esp32-c3/v2 && pio run -d packages/esp32-c3/v2 -t erase && pio run -d packages/esp32-c3/v2 -t upload", - "mcu:flash:clean": "npm run mcu:v1:flash:clean", - "desktop:install": "npm ci --prefix packages/desktop --no-audit --no-fund", - "desktop:prepare-runtime": "npm --prefix packages/desktop run prepare:runtime", - "desktop:prepare-python": "npm --prefix packages/desktop run prepare:python", - "desktop:dev": "HERMES_WEB_UI_DIR=\"$PWD\" npm --prefix packages/desktop run dev", - "build:desktop": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --publish never", - "build:desktop:mac": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --mac --publish never", - "build:desktop:win": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --win --publish never", - "build:desktop:linux": "npm run build && npm run desktop:install && npm --prefix packages/desktop run dist -- --linux --publish never", - "openapi:generate": "node scripts/generate-openapi.mjs", - "claude": "claude --dangerously-skip-permissions" + "openapi:generate": "node scripts/generate-openapi.mjs" }, "files": [ "bin/", diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue index dcb3f73..fbd8106 100644 --- a/packages/client/src/App.vue +++ b/packages/client/src/App.vue @@ -6,14 +6,11 @@ import { useI18n } from 'vue-i18n' import { getThemeOverrides } from '@/styles/theme' import { useTheme } from '@/composables/useTheme' import AppSidebar from '@/components/layout/AppSidebar.vue' -import DesktopTitleBar from '@/components/layout/DesktopTitleBar.vue' import { useKeyboard } from '@/composables/useKeyboard' import { useAppStore } from '@/stores/hermes/app' import SessionSearchModal from '@/components/hermes/chat/SessionSearchModal.vue' import AuthEventListener from '@/components/auth/AuthEventListener.vue' import DefaultCredentialPrompt from '@/components/auth/DefaultCredentialPrompt.vue' -import WebPet from '@/components/hermes/pets/WebPet.vue' -import { desktopBridge } from '@/utils/desktop-bridge' const { isDark, isComic } = useTheme() const { t } = useI18n() @@ -36,14 +33,6 @@ const nodeVersionLow = computed(() => { return !isNaN(major) && major < 23 }) -const isDesktopShell = computed(() => desktopBridge()?.isDesktop === true) -const isDesktopPetRoute = computed(() => route.name === 'desktop.pet') -const showWebPet = computed(() => !isLoginPage.value && !isDesktopShell.value && !isDesktopPetRoute.value) -const hasDesktopTitleBar = computed(() => { - const platform = desktopBridge()?.platform - return isDesktopShell.value && (platform === 'darwin' || platform === 'win32') -}) - function handleMobileMenuClick() { if (usesPageSidebar.value) { window.dispatchEvent(new CustomEvent('hermes:open-page-sidebar')) @@ -76,9 +65,7 @@ useKeyboard() - -
- +
{{ t('sidebar.nodeVersionWarning', { version: appStore.nodeVersion }) }}
@@ -93,9 +80,8 @@ useKeyboard()
- - - + +
@@ -128,10 +114,6 @@ useKeyboard() } } -.app-shell.desktop-titlebar-host .app-layout { - --vh: calc(1vh - 0.36px); -} - .app-main { flex: 1; min-width: 0; diff --git a/packages/client/src/api/client.ts b/packages/client/src/api/client.ts index 53be842..158ac29 100644 --- a/packages/client/src/api/client.ts +++ b/packages/client/src/api/client.ts @@ -3,24 +3,8 @@ import router from '@/router' const DEFAULT_BASE_URL = '' const ACTIVE_PROFILE_STORAGE_KEY = 'hermes_active_profile_name' -function isDesktopShell(): boolean { - return typeof window !== 'undefined' && - (window as typeof window & { hermesDesktop?: { isDesktop?: boolean } }).hermesDesktop?.isDesktop === true -} - -async function ensureDesktopAuthReady(): Promise { - if (typeof window === 'undefined' || getApiKey()) 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 getBaseUrl(): string { if (import.meta.env.VITE_HERMES_PREVIEW === '1') return DEFAULT_BASE_URL - if (isDesktopShell()) return DEFAULT_BASE_URL return localStorage.getItem('hermes_server_url') || DEFAULT_BASE_URL } @@ -157,7 +141,6 @@ function responseErrorMessage(text: string, statusText: string): string { } export async function request(path: string, options: RequestInit = {}): Promise { - await ensureDesktopAuthReady() const base = getBaseUrl() const url = `${base}${path}` const isFormDataBody = typeof FormData !== 'undefined' && options.body instanceof FormData diff --git a/packages/client/src/api/hermes/devices.ts b/packages/client/src/api/hermes/devices.ts index 834aa14..3704bc1 100644 --- a/packages/client/src/api/hermes/devices.ts +++ b/packages/client/src/api/hermes/devices.ts @@ -1,6 +1,6 @@ import { request } from '../client' -export type LanEndpointKind = 'web' | 'desktop' | 'custom' +export type LanEndpointKind = 'web' | 'custom' export type DeviceInboundStatus = 'none' | 'pending' | 'approved' | 'rejected' | 'blocked' export type DeviceOutboundStatus = 'none' | 'pending' | 'approved' | 'rejected' | 'blocked' diff --git a/packages/client/src/api/hermes/system.ts b/packages/client/src/api/hermes/system.ts index cae3432..4873928 100644 --- a/packages/client/src/api/hermes/system.ts +++ b/packages/client/src/api/hermes/system.ts @@ -140,6 +140,35 @@ export async function checkHealth(): Promise { return request('/health') } +export interface HermesCliStatus { + available: boolean + bin?: string +} + +let cliProbePromise: Promise | null = null + +async function probeHermesCli(): Promise { + try { + const res = await request('/api/hermes/cli-status') + return res.available !== false + } catch { + // Fail open: probe errors (older servers, mocked backends) must not block the app. + return true + } +} + +/** Cached per page load; the router guard probes once before showing the app. */ +export function checkHermesCliAvailable(): Promise { + if (!cliProbePromise) { + cliProbePromise = probeHermesCli() + } + return cliProbePromise +} + +export function resetHermesCliProbe(): void { + cliProbePromise = null +} + export async function triggerUpdate(): Promise<{ success: boolean; message: string }> { return request<{ success: boolean; message: string }>('/api/hermes/update', { method: 'POST' }) } diff --git a/packages/client/src/components/auth/DefaultCredentialPrompt.vue b/packages/client/src/components/auth/DefaultCredentialPrompt.vue index 8d9718c..e85b0b4 100644 --- a/packages/client/src/components/auth/DefaultCredentialPrompt.vue +++ b/packages/client/src/components/auth/DefaultCredentialPrompt.vue @@ -19,16 +19,7 @@ function dismissalKey(userId: number): string { return `hermes_default_credentials_prompt_dismissed_${userId}`; } -function isDesktopShell(): boolean { - return (window as typeof window & { hermesDesktop?: { isDesktop?: boolean } }).hermesDesktop?.isDesktop === true; -} - async function checkDefaultCredentials() { - if (isDesktopShell()) { - show.value = false; - return; - } - if (route.name === "login") { show.value = false; return; diff --git a/packages/client/src/components/hermes/pets/WebPet.vue b/packages/client/src/components/hermes/pets/WebPet.vue deleted file mode 100644 index 0d4bad1..0000000 --- a/packages/client/src/components/hermes/pets/WebPet.vue +++ /dev/null @@ -1,708 +0,0 @@ - - - - - diff --git a/packages/client/src/components/layout/AppSidebar.vue b/packages/client/src/components/layout/AppSidebar.vue index 585ee34..cd8e29e 100644 --- a/packages/client/src/components/layout/AppSidebar.vue +++ b/packages/client/src/components/layout/AppSidebar.vue @@ -10,7 +10,6 @@ import ModelSelector from "@/components/layout/ModelSelector.vue"; import ProfileSelector from "@/components/layout/ProfileSelector.vue"; import LanguageSwitch from "@/components/layout/LanguageSwitch.vue"; import ThemeSwitch from "@/components/layout/ThemeSwitch.vue"; -import VersionManagementModal from "@/components/layout/VersionManagementModal.vue"; import { changelog } from "@/data/changelog"; import { getStoredUsername, isStoredSuperAdmin } from "@/api/client"; @@ -25,11 +24,7 @@ const selectedKey = computed(() => { const isSuperAdmin = computed(() => isStoredSuperAdmin()); const currentUsername = computed(() => getStoredUsername()); const isVersionPreview = import.meta.env.VITE_HERMES_PREVIEW === '1'; -const isDesktopShell = computed(() => - (window as typeof window & { hermesDesktop?: { isDesktop?: boolean } }).hermesDesktop?.isDesktop === true, -); const showChangelog = ref(false); -const showVersionManagement = ref(false); const showDockerUpdateTip = ref(false); const isDockerRuntime = computed(() => appStore.isDocker); @@ -87,10 +82,6 @@ function openChangelog() { showChangelog.value = true; } -function openVersionManagement() { - showVersionManagement.value = true; -} - function handleDockerUpdateTip() { showDockerUpdateTip.value = true; } @@ -361,20 +352,6 @@ function handleUpdateClick() { - - - {{ t('sidebar.versionManagement') }} - {{ t('sidebar.updateAvailableLabel') }} - - {{ t('sidebar.reloadClientVersion', { version: appStore.serverVersion }) }} @@ -422,7 +399,6 @@ function handleUpdateClick() { -
@@ -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 @@ - - - - - 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 @@ - - - - - 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() {