[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
This commit is contained in:
ekko
2026-05-20 14:15:01 +08:00
committed by GitHub
parent 663afb61ff
commit c90eba226d
27 changed files with 892 additions and 94 deletions
@@ -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<string | null>(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(() => {
</template>
<template v-else>
<div class="msg-body">
<img
<ProfileAvatar
v-if="message.role === 'assistant'"
src="/logo.png"
alt="Hermes"
class="msg-avatar"
:name="assistantProfileName"
:avatar="assistantProfileAvatar"
:size="40"
/>
<div class="msg-content" :class="message.role">
<div
@@ -2,9 +2,10 @@
import { computed, ref, onUnmounted } from 'vue'
import { NPopconfirm, NCheckbox } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import multiavatar from '@multiavatar/multiavatar'
import type { Session } from '@/stores/hermes/chat'
import { useAppStore } from '@/stores/hermes/app'
import { useProfilesStore } from '@/stores/hermes/profiles'
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
import { formatTimestampMs } from '@/shared/session-display'
const props = withDefaults(defineProps<{
@@ -29,13 +30,14 @@ const emit = defineEmits<{
const { t } = useI18n()
const appStore = useAppStore()
const profilesStore = useProfilesStore()
const sessionModelName = computed(() =>
props.session.model
? appStore.displayModelName(props.session.model, props.session.provider)
: '',
)
const profileName = computed(() => props.session.profile || 'default')
const profileAvatar = computed(() => multiavatar(profileName.value))
const profileAvatar = computed(() => profilesStore.profiles.find(profile => profile.name === profileName.value)?.avatar)
let longPressTimer: ReturnType<typeof setTimeout> | null = null
const longPressTriggered = ref(false)
@@ -114,7 +116,7 @@ onUnmounted(() => {
<span class="session-item-time">{{ formatTimestampMs(session.createdAt) }}</span>
</span>
<span v-if="props.showProfile" class="session-item-profile">
<span class="session-item-profile-avatar" v-html="profileAvatar" />
<ProfileAvatar class="session-item-profile-avatar" :name="profileName" :avatar="profileAvatar" :size="16" />
<span class="session-item-profile-name">{{ profileName }}</span>
</span>
</div>
@@ -139,18 +141,7 @@ onUnmounted(() => {
}
.session-item-profile-avatar {
display: inline-flex;
width: 16px;
height: 16px;
flex: 0 0 16px;
border-radius: 50%;
overflow: hidden;
}
.session-item-profile-avatar :deep(svg) {
width: 16px;
height: 16px;
display: block;
background: var(--bg-secondary);
}
.session-item-profile-name {