fix(chat): group run output and hide empty messages
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

This commit is contained in:
2026-11-05 11:06:36 +08:00
parent 586776998b
commit 276e2389d6
8 changed files with 155 additions and 3 deletions
@@ -19,6 +19,7 @@ import { ref, computed, nextTick, onBeforeUnmount, onMounted, watch } from "vue"
import { useI18n } from "vue-i18n";
import VirtualMessageList from "./VirtualMessageList.vue";
import MessageItem from "./MessageItem.vue";
import { groupRunMessages } from "./groupRunMessages";
import { LIVE_CHAT_MAX_LOADED_MESSAGES, useChatStore, type Message } from "@/stores/hermes/chat";
import thinkingImage from "@/assets/thinking.gif";
import { useToolTraceVisibility } from "@/composables/useToolTraceVisibility";
@@ -100,10 +101,15 @@ const emptyState = computed(() => ({
const displayMessages = computed(() => {
const currentToolIds = new Set(currentToolCalls.value.map((tool) => tool.id));
return chatStore.messages.filter((m) => {
const messages = chatStore.messages.filter((m) => {
if (m.role === "tool") {
return toolTraceVisible.value && !!m.toolName && !(chatStore.isRunActive && currentToolIds.has(m.id));
}
// Tool cycles can produce whitespace-only assistant deltas. Keep them in
// the store for subsequent deltas, but don't render an empty bubble.
if (m.role === "assistant" && !m.content?.trim() && !m.reasoning?.trim() && !m.attachments?.length) {
return false;
}
if (
m.role === "assistant" &&
m.isStreaming &&
@@ -115,6 +121,7 @@ const displayMessages = computed(() => {
}
return true;
});
return groupRunMessages(messages, chatStore.isRunActive || !!chatStore.abortState);
});
function forkDividerId(sessionId: string): string {
@@ -484,7 +491,8 @@ defineExpose({
</div>
</template>
<template #item="{ message: msg }">
<div v-if="msg.systemType === 'fork-divider' && forkLineage" class="fork-divider" role="separator">
<div v-if="msg.systemType === 'run-end'" class="run-end" role="separator">{{ t('chat.runOutputEnded') }}</div>
<div v-else-if="msg.systemType === 'fork-divider' && forkLineage" class="fork-divider" role="separator">
<div class="fork-divider-line" aria-hidden="true"></div>
<div class="fork-divider-pill">
<span class="fork-divider-icon" aria-hidden="true">
@@ -867,6 +875,13 @@ defineExpose({
<style scoped lang="scss">
@use "@/styles/variables" as *;
.run-end {
margin: 20px auto;
text-align: center;
color: var(--color-text-3);
font-size: 12px;
}
.message-list-shell {
flex: 1;
min-height: 0;
@@ -0,0 +1,45 @@
import type { Message } from '@/stores/hermes/chat'
// Group only within an uninterrupted turn. Tool rows remain available after
// the combined answer; user, command and system messages are hard boundaries.
export function groupRunMessages(messages: Message[], runActive: boolean): Message[] {
const result: Message[] = []
let assistants: Message[] = []
let tools: Message[] = []
let marker: string | null = null
function flush(completed: boolean) {
if (assistants.length) {
const first = assistants[0]
result.push({
...first,
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,
attachments: assistants.flatMap(m => m.attachments || []),
isStreaming: assistants.some(m => m.isStreaming),
})
}
result.push(...tools)
if (completed && assistants.length) {
result.push({ id: `run-end-${assistants[0].id}`, role: 'system', content: '', timestamp: assistants[assistants.length - 1].timestamp, systemType: 'run-end' })
}
assistants = []
tools = []
marker = null
}
for (const message of messages) {
if (message.role !== 'assistant' && message.role !== 'tool') {
flush(true)
result.push(message)
continue
}
if (marker && message.runMarker && marker !== message.runMarker) flush(true)
marker ||= message.runMarker || null
if (message.role === 'assistant') assistants.push(message)
else tools.push(message)
}
flush(!runActive)
return result
}
+1
View File
@@ -459,6 +459,7 @@ export default {
// Chat
chat: {
runOutputEnded: 'Run output ended',
messageLoadFailed: 'Could not load this conversation. Please retry.',
contextRemaining: 'remaining',
contextClickToEdit: 'Click to edit context length',
+1
View File
@@ -459,6 +459,7 @@ export default {
// 对话
chat: {
runOutputEnded: '本轮输出结束',
messageLoadFailed: '会话加载失败,请重试。',
contextRemaining: '剩余',
contextClickToEdit: '点击编辑上下文长度',
+1 -1
View File
@@ -63,7 +63,7 @@ export interface Message {
// 不含 <think> 包裹标签;内容自身可以为多段纯文本。
reasoning?: string
queued?: boolean
systemType?: 'command' | 'error' | 'fork-divider'
systemType?: 'command' | 'error' | 'fork-divider' | 'run-end'
commandAction?: string
commandData?: Record<string, unknown>
finishReason?: string | null