Files
Hermes-ui/packages/server/src/services/hermes/hermes-profile.ts
T
jsonetandekko 7e7fe90483 fix(chat): isolate concurrent session events and workspace dialog i18n (#351)
* feat: per-session workspace with folder picker, HERMES_HOME support, esbuild fix

* fix(chat): isolate concurrent session events and workspace dialog i18n

Two user-visible bugs are fixed here:

1. Workspace dialog title showed the raw i18n key 'chat.setWorkspaceTitle' because the key was never added to en.ts / zh.ts. The dialog is opened from ChatPanel.vue but only 'setWorkspace' existed. Add the missing 'setWorkspaceTitle' translation in both locales.

2. With two concurrent runs the assistant text from session A would show up in session B (and vice versa). The /chat-run namespace uses a single shared Socket.IO connection on the client; every startRunViaSocket() call registers its own listeners on the same socket. The server fans events out via 'session:<id>' rooms, but a single socket can be in multiple rooms at once and there was no per-event filtering on the client. Each run's closure captured its own sid and wrote into the wrong session. The server already tags every payload with session_id, so the fix is a guard inside handleEvent() that drops events whose session_id does not match this run's body.session_id. Untagged events are still accepted for backwards compatibility.

3. Also fix a related crash where setting a workspace on a session that had not been persisted yet (no first message sent) threw because the row did not exist. Create the row on demand inside setWorkspace controller.

* fix: upgrade esbuild to 0.27+ for vite 8 compatibility

---------

Co-authored-by: ekko <fqsy1416@gmail.com>
2026-04-30 20:17:38 +08:00

68 lines
1.7 KiB
TypeScript

import { resolve, join } from 'path'
import { homedir } from 'os'
import { readFileSync, existsSync } from 'fs'
const HERMES_BASE = process.env.HERMES_HOME || resolve(homedir(), '.hermes')
/**
* Get the active profile's home directory.
* default → ~/.hermes/
* other → ~/.hermes/profiles/{name}/
*/
export function getActiveProfileDir(): string {
const activeFile = join(HERMES_BASE, 'active_profile')
try {
const name = readFileSync(activeFile, 'utf-8').trim()
if (name && name !== 'default') {
const dir = join(HERMES_BASE, 'profiles', name)
if (existsSync(dir)) return dir
}
} catch { }
return HERMES_BASE
}
/**
* Get the active profile's config.yaml path.
*/
export function getActiveConfigPath(): string {
return join(getActiveProfileDir(), 'config.yaml')
}
/**
* Get the active profile's auth.json path.
*/
export function getActiveAuthPath(): string {
return join(getActiveProfileDir(), 'auth.json')
}
/**
* Get the active profile's .env path.
*/
export function getActiveEnvPath(): string {
return join(getActiveProfileDir(), '.env')
}
/**
* Get the active profile name.
*/
export function getActiveProfileName(): string {
const activeFile = join(HERMES_BASE, 'active_profile')
try {
const name = readFileSync(activeFile, 'utf-8').trim()
return name || 'default'
} catch {
return 'default'
}
}
/**
* Get profile directory by name.
* default → ~/.hermes/
* other → ~/.hermes/profiles/{name}/
*/
export function getProfileDir(name: string): string {
if (!name || name === 'default') return HERMES_BASE
const dir = join(HERMES_BASE, 'profiles', name)
return existsSync(dir) ? dir : HERMES_BASE
}