fix(chat): place agent activity before response
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-11-05 12:27:22 +08:00
parent 7ee844cd88
commit 43eda89d47
7 changed files with 84 additions and 42 deletions
@@ -35,7 +35,7 @@ const JSON_MAX_KEYS_PER_OBJECT = 50;
const JSON_MAX_ITEMS_PER_ARRAY = 50;
const JSON_TRUNCATED_KEY = "__truncated__";
const props = defineProps<{ message: Message; highlight?: boolean; headingIdPrefix?: string; showForkAction?: boolean }>();
const props = defineProps<{ message: Message; highlight?: boolean; headingIdPrefix?: string; showForkAction?: boolean; activityOnly?: boolean }>();
const { t } = useI18n();
const toast = useMessage();
@@ -1021,7 +1021,8 @@ onBeforeUnmount(() => {
/>
<span class="msg-profile-name" :title="assistantProfileName">{{ assistantProfileName }}</span>
</div>
<div class="msg-content" :class="message.role">
<slot name="agent-activity" />
<div v-if="!activityOnly" class="msg-content" :class="message.role">
<div
class="message-bubble"
:class="{
@@ -120,7 +120,22 @@ const displayMessages = computed(() => {
}
return true;
});
return groupRunMessages(messages, chatStore.isRunActive || !!chatStore.abortState);
const grouped = groupRunMessages(messages, chatStore.isRunActive || !!chatStore.abortState);
if (isThinkingIndicatorVisible.value) {
const lastInput = grouped.findLastIndex(m => m.role === 'user' || m.role === 'command');
const currentAssistant = grouped.slice(lastInput + 1).findLast(m => m.role === 'assistant');
if (!currentAssistant) {
grouped.push({ id: `run-activity-${chatStore.activeSessionId}`, role: 'assistant', content: '', timestamp: 0 });
}
}
return grouped;
});
const activeAssistantId = computed(() => {
if (!isThinkingIndicatorVisible.value) return null;
const messages = displayMessages.value;
const lastInput = messages.findLastIndex(m => m.role === 'user' || m.role === 'command');
return messages.slice(lastInput + 1).findLast(m => m.role === 'assistant')?.id ?? null;
});
function forkDividerId(sessionId: string): string {
@@ -519,18 +534,15 @@ defineExpose({
:message="msg"
:highlight="chatStore.focusMessageId === msg.id"
:show-fork-action="canForkActiveSession && msg.id === lastForkActionMessageId"
/>
</template>
<template #after>
<Transition name="fade">
<div v-if="isThinkingIndicatorVisible" class="streaming-indicator">
:activity-only="msg.id === `run-activity-${chatStore.activeSessionId}`"
>
<template #agent-activity>
<details v-if="msg.toolMessages?.length" class="tool-group">
<summary>{{ t('chat.toolCallsGroup', { count: msg.toolMessages.length }) }}</summary>
<MessageItem v-for="tool in msg.toolMessages" :key="tool.id" :message="tool" />
</details>
<div v-if="msg.id === activeAssistantId" class="streaming-indicator">
<div class="thinking-status">
<img
src="/app-logo.png"
alt=""
aria-hidden="true"
class="thinking-avatar"
>
<div class="thinking-status-copy">
<span class="thinking-status-label">{{ t("chat.thinkingInProgress") }}</span>
<span class="thinking-status-time">{{ formattedThinkingElapsed }}</span>
@@ -696,7 +708,8 @@ defineExpose({
</details>
</div>
</div>
</Transition>
</template>
</MessageItem>
</template>
</VirtualMessageList>
<button
@@ -1475,22 +1488,8 @@ defineExpose({
min-height: 40px;
}
.thinking-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
animation: thinking-avatar-breathe 2.4s ease-in-out infinite;
}
@keyframes thinking-avatar-breathe {
0%, 100% { transform: translateY(0) scale(1); filter: drop-shadow(0 0 2px rgba(var(--accent-primary-rgb), 0.12)); }
50% { transform: translateY(-2px) scale(1.06); filter: drop-shadow(0 0 7px rgba(var(--accent-primary-rgb), 0.4)); }
}
@media (prefers-reduced-motion: reduce) {
.thinking-status .thinking-avatar, .thinking-status .thinking-status-label { animation: none; }
.thinking-status .thinking-status-label { animation: none; }
}
.thinking-status-copy {
@@ -13,6 +13,7 @@ export function groupRunMessages(messages: Message[], runActive: boolean): Messa
const first = assistants[0]
result.push({
...first,
toolMessages: tools.length ? tools : undefined,
id: assistants[assistants.length - 1].id,
content: assistants.map(m => m.content).filter(text => text.trim()).join('\n\n'),
reasoning: assistants.map(m => m.reasoning).filter(Boolean).join('\n\n') || undefined,
@@ -20,7 +21,7 @@ export function groupRunMessages(messages: Message[], runActive: boolean): Messa
isStreaming: assistants.some(m => m.isStreaming),
})
}
if (tools.length) {
if (tools.length && !assistants.length) {
result.push({ id: `tool-group-${tools[0].id}`, role: 'system', content: '', timestamp: tools[0].timestamp, systemType: 'tool-group', toolMessages: tools })
}
if (completed && assistants.length) {