Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
date: 2026-06-12
pr: 1120
commit: f67effa
feature: 自定义思考动画 + 头像大小可调(per profile)
impact: 不改变 /chat-run 协议、消息落库或 run 生命周期;仅影响流式输出指示器的动画图片和聊天消息中头像的渲染尺寸。
---

`MessageList.vue` 引入 `profilesStore`,通过 `thinkingImageUrl` computed 读取当前 profile 的自定义思考动画(支持关闭),fallback 到内置 dark/light GIF;当用户关闭 thinking animation 时流式指示器不显示。

`MessageItem.vue` 读取 profile avatar 的 `avatar_size` 字段(默认 40px),将 assistant 头像从固定尺寸改为动态绑定:`:size` prop + inline `:style` 覆盖 scoped CSS。
24 changes: 24 additions & 0 deletions packages/client/src/api/hermes/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface HermesProfile {
gatewayStatus?: string
alias: string
avatar?: ProfileAvatar | null
thinkingAnimation?: ProfileThinkingAnimation | null
}

export interface HermesProfileDetail {
Expand All @@ -18,15 +19,25 @@ export interface HermesProfileDetail {
hasEnv: boolean
hasSoulMd: boolean
avatar?: ProfileAvatar | null
thinkingAnimation?: ProfileThinkingAnimation | null
}

export interface ProfileAvatar {
type: 'generated' | 'image'
seed?: string
dataUrl?: string
avatar_size?: number
updatedAt?: number
}

export interface ProfileThinkingAnimation {
url?: string
enableThinking?: boolean
updatedAt?: number
items?: Array<{ url: string; name?: string }>
activeIndex?: number
}

export interface ProfileRuntimeStatus {
profile: string
bridge: {
Expand Down Expand Up @@ -226,3 +237,16 @@ export async function importProfile(file: File): Promise<boolean> {
return false
}
}

export async function updateProfileThinkingAnimation(name: string, animation: ProfileThinkingAnimation): Promise<ProfileThinkingAnimation> {
const res = await request<{ animation: ProfileThinkingAnimation }>(`/api/hermes/profiles/${encodeURIComponent(name)}/thinking-animation`, {
method: 'PUT',
body: JSON.stringify(animation),
})
return res.animation
}

export async function deleteProfileThinkingAnimation(name: string): Promise<void> {
await request(`/api/hermes/profiles/${encodeURIComponent(name)}/thinking-animation`, { method: 'DELETE' })
}

9 changes: 6 additions & 3 deletions packages/client/src/components/hermes/chat/MessageItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ 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);
const assistantAvatarSize = computed(() => {
const avatar = assistantProfileAvatar.value;
return avatar?.avatar_size ?? 40;
});

// Copy entire bubble content
const copyableContent = computed(() => {
Expand Down Expand Up @@ -820,7 +824,8 @@ onBeforeUnmount(() => {
class="msg-avatar"
:name="assistantProfileName"
:avatar="assistantProfileAvatar"
:size="40"
:size="assistantAvatarSize"
:style="{ width: assistantAvatarSize + 'px', height: assistantAvatarSize + 'px' }"
/>
<div class="msg-content" :class="message.role">
<div
Expand Down Expand Up @@ -1073,8 +1078,6 @@ onBeforeUnmount(() => {
}

.msg-avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
margin-top: 2px;
}
Expand Down
15 changes: 13 additions & 2 deletions packages/client/src/components/hermes/chat/MessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import MessageItem from "./MessageItem.vue";
import { LIVE_CHAT_MAX_LOADED_MESSAGES, useChatStore } from "@/stores/hermes/chat";
import thinkingImage from "@/assets/thinking.gif";
import { useToolTraceVisibility } from "@/composables/useToolTraceVisibility";
import { useProfilesStore } from "@/stores/hermes/profiles";

const chatStore = useChatStore();
const profilesStore = useProfilesStore();
const { t } = useI18n();
const { toolTraceVisible } = useToolTraceVisibility();
const listRef = ref<InstanceType<typeof VirtualMessageList> | null>(null);
Expand Down Expand Up @@ -173,6 +175,15 @@ function handleClarify(response?: string) {
clarifyResponse.value = "";
}

const thinkingImageUrl = computed(() => {
const ta = profilesStore.activeProfile?.thinkingAnimation;
const enabled = ta?.enableThinking ?? true;
if (!enabled) return null;
const custom = ta?.url;
if (custom) return custom;
return thinkingImage;
});

function removeQueuedMessage(messageId: string) {
const sid = chatStore.activeSessionId;
if (!sid) return;
Expand Down Expand Up @@ -427,10 +438,10 @@ defineExpose({
</template>
<template #after>
<Transition name="fade">
<div v-if="isThinkingIndicatorVisible" class="streaming-indicator">
<div v-if="isThinkingIndicatorVisible && thinkingImageUrl" class="streaming-indicator">
<div class="thinking-status">
<img
:src="thinkingImage"
:src="thinkingImageUrl"
alt=""
aria-hidden="true"
class="thinking-avatar"
Expand Down
Loading
Loading