diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8c0034f..74d5020 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -31,6 +31,7 @@ should not duplicate server persistence rules. - `HERMES_WEB_UI_HOME` and `HERMES_WEBUI_STATE_DIR` override Web UI state location. - Hermes Agent state lives under Hermes profile directories and must stay distinct from Web UI state. - Uploads default to `config.uploadDir`, which is derived from the Web UI home unless `UPLOAD_DIR` is set. +- Runtime data directories must also live under the Web UI home, not beside built `dist` assets. - Profile-scoped Hermes data should use existing profile helpers instead of manually joining paths. ## Server Structure diff --git a/packages/desktop/build/icons/128x128.png b/packages/desktop/build/icons/128x128.png new file mode 100644 index 0000000..8c1407d Binary files /dev/null and b/packages/desktop/build/icons/128x128.png differ diff --git a/packages/desktop/build/icons/16x16.png b/packages/desktop/build/icons/16x16.png new file mode 100644 index 0000000..0f44785 Binary files /dev/null and b/packages/desktop/build/icons/16x16.png differ diff --git a/packages/desktop/build/icons/256x256.png b/packages/desktop/build/icons/256x256.png new file mode 100644 index 0000000..31ca586 Binary files /dev/null and b/packages/desktop/build/icons/256x256.png differ diff --git a/packages/desktop/build/icons/32x32.png b/packages/desktop/build/icons/32x32.png new file mode 100644 index 0000000..df8f031 Binary files /dev/null and b/packages/desktop/build/icons/32x32.png differ diff --git a/packages/desktop/build/icons/48x48.png b/packages/desktop/build/icons/48x48.png new file mode 100644 index 0000000..6bb9be4 Binary files /dev/null and b/packages/desktop/build/icons/48x48.png differ diff --git a/packages/desktop/build/icons/512x512.png b/packages/desktop/build/icons/512x512.png new file mode 100644 index 0000000..5815f8d Binary files /dev/null and b/packages/desktop/build/icons/512x512.png differ diff --git a/packages/desktop/build/icons/64x64.png b/packages/desktop/build/icons/64x64.png new file mode 100644 index 0000000..b286ee6 Binary files /dev/null and b/packages/desktop/build/icons/64x64.png differ diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index 2b3db1e..86313d3 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -21,6 +21,10 @@ files: # Web UI source (built dist) and bundled Python live outside the asar. # This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root. extraResources: + - from: "build" + to: "build" + filter: + - "icon.png" - from: "../.." to: "webui" filter: @@ -57,6 +61,7 @@ win: artifactName: "${productName}-${version}-${arch}.${ext}" linux: + icon: build/icons target: - target: AppImage arch: [x64, arm64] diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 03a2087..86a227e 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -1,7 +1,7 @@ import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron' import { join } from 'node:path' import { startWebUiServer, stopWebUiServer, getToken } from './webui-server' -import { hermesBinExists, hermesBin } from './paths' +import { desktopIcon, hermesBinExists, hermesBin } from './paths' import { initAutoUpdater } from './updater' const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748 @@ -18,6 +18,7 @@ function createWindow() { title: 'Hermes Studio', backgroundColor: '#1a1a1a', autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon: desktopIcon() } : {}), webPreferences: { preload: join(__dirname, '..', 'preload', 'index.js'), contextIsolation: true, diff --git a/packages/desktop/src/main/paths.ts b/packages/desktop/src/main/paths.ts index 88b2f3f..15bb5fb 100644 --- a/packages/desktop/src/main/paths.ts +++ b/packages/desktop/src/main/paths.ts @@ -40,6 +40,11 @@ export function hermesBinExists(): boolean { return existsSync(hermesBin()) } +export function desktopIcon(): string { + if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'icon.png') + return resolve(app.getAppPath(), 'build', 'icon.png') +} + export function webUiHome(): string { return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui') } diff --git a/packages/desktop/src/main/webui-server.ts b/packages/desktop/src/main/webui-server.ts index 160a319..93b8a1c 100644 --- a/packages/desktop/src/main/webui-server.ts +++ b/packages/desktop/src/main/webui-server.ts @@ -147,6 +147,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise { // macOS/Linux keep the standard ~/.hermes layout. HERMES_HOME: agentHome, HERMES_WEB_UI_HOME: home, + HERMES_WEBUI_STATE_DIR: home, AUTH_TOKEN: token, PORT: String(port), // Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours diff --git a/packages/server/src/config.ts b/packages/server/src/config.ts index ab8dfef..654e6da 100644 --- a/packages/server/src/config.ts +++ b/packages/server/src/config.ts @@ -14,6 +14,7 @@ import { homedir } from 'os' * - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME. * Default: join(homedir(), '.hermes-web-ui'). * - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload'). + * - dataDir: Internal Web UI runtime data directory. Default: join(HERMES_WEB_UI_HOME, 'data'). * * Auth: * - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME. @@ -41,6 +42,10 @@ export function getWebUiHome(env: Record = process.e return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui') } +export function getWebUiDataDir(env: Record = process.env): string { + return join(getWebUiHome(env), 'data') +} + const appHome = getWebUiHome() export const config = { @@ -49,6 +54,6 @@ export const config = { host: getListenHost(), appHome, uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'), - dataDir: resolve(__dirname, '..', 'data'), + dataDir: getWebUiDataDir(), corsOrigins: process.env.CORS_ORIGINS || '*', } diff --git a/scripts/harness-check.mjs b/scripts/harness-check.mjs index 1b89d2f..bd6ab97 100644 --- a/scripts/harness-check.mjs +++ b/scripts/harness-check.mjs @@ -42,6 +42,7 @@ for (const dir of [ 'packages/client/src', 'packages/server/src', 'packages/desktop', + 'packages/desktop/build/icons', 'tests/client', 'tests/server', 'tests/e2e', @@ -50,6 +51,21 @@ for (const dir of [ requireDir(dir) } +for (const icon of [ + 'packages/desktop/build/icon.png', + 'packages/desktop/build/icon.icns', + 'packages/desktop/build/icon.ico', + 'packages/desktop/build/icons/16x16.png', + 'packages/desktop/build/icons/32x32.png', + 'packages/desktop/build/icons/48x48.png', + 'packages/desktop/build/icons/64x64.png', + 'packages/desktop/build/icons/128x128.png', + 'packages/desktop/build/icons/256x256.png', + 'packages/desktop/build/icons/512x512.png', +]) { + requireFile(icon) +} + const agents = await readText('AGENTS.md') const agentLines = agents.trimEnd().split(/\r?\n/) if (agentLines.length > 120) { @@ -101,10 +117,15 @@ if (!buildWorkflow.includes('npm run harness:check')) { } const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml') +const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml') if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) { fail('desktop-release.yml must upload matrix-specific artifact_files') } +if (!electronBuilderConfig.includes('icon: build/icons')) { + fail('electron-builder.yml must configure the Linux icon set') +} + for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) { if (!desktopReleaseWorkflow.includes(target)) { fail(`desktop-release.yml is missing matrix target ${target}`) diff --git a/tests/server/config.test.ts b/tests/server/config.test.ts index de35253..9c3cc0f 100644 --- a/tests/server/config.test.ts +++ b/tests/server/config.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { homedir } from 'os' import { join, resolve } from 'path' -import { getListenHost, getWebUiHome } from '../../packages/server/src/config' +import { getListenHost, getWebUiDataDir, getWebUiHome } from '../../packages/server/src/config' describe('server config', () => { it('defaults to an IPv4 bind host', () => { @@ -27,4 +27,8 @@ describe('server config', () => { it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => { expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state')) }) + + it('keeps runtime data under the web-ui home', () => { + expect(getWebUiDataDir({ HERMES_WEB_UI_HOME: ' ./tmp/hermes-ui ' })).toBe(resolve('./tmp/hermes-ui/data')) + }) })