From c90eba226d69d399ca7da9461e4df29b1f81b482 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Wed, 20 May 2026 14:15:01 +0800 Subject: [PATCH] [codex] add customizable profile avatars (#870) * add customizable profile avatars * keep profile avatar visible when sidebar collapses * simplify collapsed profile avatar styling * force managed gateway startup in docker * limit gateway autostart to active profile * restore all profile gateway autostart * fix managed gateway runtime detection --- Dockerfile | 1 + docker-compose.yml | 1 + packages/client/src/api/hermes/profiles.ts | 21 ++ .../components/hermes/chat/MessageItem.vue | 12 +- .../hermes/chat/SessionListItem.vue | 21 +- .../hermes/group-chat/GroupChatPanel.vue | 33 +-- .../hermes/group-chat/GroupMessageItem.vue | 18 +- .../hermes/profiles/ProfileAvatar.vue | 56 +++++ .../hermes/profiles/ProfileCard.vue | 17 +- .../src/components/layout/AppSidebar.vue | 38 ++- .../src/components/layout/ProfileSelector.vue | 220 +++++++++++++++--- packages/client/src/i18n/locales/de.ts | 14 ++ packages/client/src/i18n/locales/en.ts | 14 ++ packages/client/src/i18n/locales/es.ts | 14 ++ packages/client/src/i18n/locales/fr.ts | 14 ++ packages/client/src/i18n/locales/ja.ts | 14 ++ packages/client/src/i18n/locales/ko.ts | 14 ++ packages/client/src/i18n/locales/pt.ts | 14 ++ packages/client/src/i18n/locales/zh-TW.ts | 14 ++ packages/client/src/i18n/locales/zh.ts | 14 ++ packages/client/src/stores/hermes/profiles.ts | 29 +++ .../server/src/controllers/hermes/profiles.ts | 166 ++++++++++++- packages/server/src/routes/hermes/profiles.ts | 2 + .../src/services/hermes/gateway-autostart.ts | 75 +++++- tests/client/profiles-store.test.ts | 50 ++++ tests/server/gateway-autostart.test.ts | 32 +++ tests/server/profiles-routes.test.ts | 68 ++++++ 27 files changed, 892 insertions(+), 94 deletions(-) create mode 100644 packages/client/src/components/hermes/profiles/ProfileAvatar.vue diff --git a/Dockerfile b/Dockerfile index 7d91a3a..1804d23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ RUN npm run build && npm prune --omit=dev ENV NODE_ENV=production ENV HOME=/home/agent ENV HERMES_HOME=/home/agent/.hermes +ENV HERMES_WEB_UI_MANAGED_GATEWAY=1 ENV PATH=/opt/hermes/.venv/bin:$PATH EXPOSE 6060 diff --git a/docker-compose.yml b/docker-compose.yml index 3c41686..a7dd865 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - PORT=${PORT:-6060} - HERMES_HOME=/home/agent/.hermes - HERMES_BIN=/opt/hermes/.venv/bin/hermes + - HERMES_WEB_UI_MANAGED_GATEWAY=1 - HERMES_WEB_UI_XAI_CALLBACK_BIND_HOST=0.0.0.0 - PATH=/opt/hermes/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - AUTH_DISABLED=${AUTH_DISABLED:-false} diff --git a/packages/client/src/api/hermes/profiles.ts b/packages/client/src/api/hermes/profiles.ts index 53c2071..3498824 100644 --- a/packages/client/src/api/hermes/profiles.ts +++ b/packages/client/src/api/hermes/profiles.ts @@ -6,6 +6,7 @@ export interface HermesProfile { model: string gatewayStatus?: string alias: string + avatar?: ProfileAvatar | null } export interface HermesProfileDetail { @@ -16,6 +17,14 @@ export interface HermesProfileDetail { skills: number hasEnv: boolean hasSoulMd: boolean + avatar?: ProfileAvatar | null +} + +export interface ProfileAvatar { + type: 'generated' | 'image' + seed?: string + dataUrl?: string + updatedAt?: number } export interface ProfileRuntimeStatus { @@ -62,6 +71,18 @@ export async function fetchProfileRuntimeStatuses(): Promise { + const res = await request<{ avatar: ProfileAvatar }>(`/api/hermes/profiles/${encodeURIComponent(name)}/avatar`, { + method: 'PUT', + body: JSON.stringify(avatar), + }) + return res.avatar +} + +export async function deleteProfileAvatar(name: string): Promise { + await request(`/api/hermes/profiles/${encodeURIComponent(name)}/avatar`, { method: 'DELETE' }) +} + export async function restartProfileGateway(name: string): Promise { const res = await request<{ success: boolean; gateway: ProfileRuntimeStatus['gateway'] }>( `/api/hermes/profiles/${encodeURIComponent(name)}/gateway/restart`, diff --git a/packages/client/src/components/hermes/chat/MessageItem.vue b/packages/client/src/components/hermes/chat/MessageItem.vue index 1daaee1..4b6878a 100644 --- a/packages/client/src/components/hermes/chat/MessageItem.vue +++ b/packages/client/src/components/hermes/chat/MessageItem.vue @@ -9,7 +9,9 @@ import { copyToClipboard } from "@/utils/clipboard"; import MarkdownRenderer from "./MarkdownRenderer.vue"; import { parseThinking, countThinkingChars } from "@/utils/thinking-parser"; import { useChatStore } from "@/stores/hermes/chat"; +import { useProfilesStore } from "@/stores/hermes/profiles"; import { useSettingsStore } from "@/stores/hermes/settings"; +import ProfileAvatar from "@/components/hermes/profiles/ProfileAvatar.vue"; import { copyTextToClipboard, handleCodeBlockCopyClick, @@ -180,9 +182,12 @@ const toolExpanded = ref(false); const previewUrl = ref(null); const chatStore = useChatStore(); +const profilesStore = useProfilesStore(); const settingsStore = useSettingsStore(); const speech = useGlobalSpeech(); const voiceSettings = useVoiceSettings(); +const assistantProfileName = computed(() => chatStore.activeSession?.profile || profilesStore.activeProfileName || "default"); +const assistantProfileAvatar = computed(() => profilesStore.profiles.find(profile => profile.name === assistantProfileName.value)?.avatar); // Copy entire bubble content const copyableContent = computed(() => { @@ -773,11 +778,12 @@ onBeforeUnmount(() => {