fix: 修复流式输出展开状态被覆盖

- 保留智能体思考内容的手动展开与关闭选择

- 工作流试运行默认跟随当前节点,并允许用户自由多开或关闭
This commit is contained in:
2026-07-29 00:45:46 +08:00
parent f392c896f9
commit 5ee6065017
4 changed files with 271 additions and 14 deletions

View File

@@ -7,7 +7,7 @@ import type {
ChatTimelineToolApprovalPayload,
} from './types';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import ChatThinkingBlock from '../chat-thinking/ChatThinkingBlock.vue';
import ChatErrorNotice from './ChatErrorNotice.vue';
@@ -43,6 +43,8 @@ const emit = defineEmits<{
const messageItem = computed(() =>
props.item.type === 'message' ? props.item : undefined,
);
// 流式快照会替换消息对象,用户手动选择需要由稳定的组件实例单独保留。
const thinkingExpandedOverrides = ref<Record<string, boolean>>({});
const alignmentClass = computed(() => {
if (props.item.type === 'message' && props.item.role === 'user') {
@@ -115,11 +117,25 @@ function updateThinkingExpanded(partId: string, expanded: boolean) {
}
const part = item.parts.find((current) => current.id === partId);
if (part?.type === 'thinking') {
thinkingExpandedOverrides.value = {
...thinkingExpandedOverrides.value,
[partId]: expanded,
};
emit('thinkingToggle');
part.expanded = expanded;
}
}
function resolveThinkingExpanded(
part: Extract<ChatTimelineMessagePart, { type: 'thinking' }>,
) {
return (
thinkingExpandedOverrides.value[part.id] ??
part.expanded ??
part.status === 'thinking'
);
}
function handleCopyAction() {
const item = messageItem.value;
if (!item || !props.copyAction) {
@@ -149,7 +165,7 @@ function handleCopyAction() {
<ChatThinkingBlock
v-if="part.type === 'thinking'"
:content="part.content"
:expanded="part.expanded ?? part.status === 'thinking'"
:expanded="resolveThinkingExpanded(part)"
:status="part.status"
class="chat-timeline-item__thinking"
readonly

View File

@@ -0,0 +1,91 @@
import type { ChatTimelineItem } from '../types';
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import ChatTimeline from '../ChatTimeline.vue';
function thinkingMessage(options?: {
content?: string;
expanded?: boolean;
messageStatus?: 'done' | 'streaming';
thinkingStatus?: 'end' | 'thinking';
}): Extract<ChatTimelineItem, { type: 'message' }> {
return {
id: 'assistant-thinking',
parts: [
{
id: 'thinking-part',
content: options?.content ?? '分析问题',
expanded: options?.expanded ?? true,
status: options?.thinkingStatus ?? 'thinking',
type: 'thinking',
},
],
role: 'assistant',
status: options?.messageStatus ?? 'streaming',
type: 'message',
};
}
describe('chat timeline thinking interaction', () => {
it('keeps the user expansion choice across streaming snapshots', async () => {
const wrapper = mount(ChatTimeline, {
props: {
items: [thinkingMessage()],
},
});
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(true);
await wrapper.get('.chat-thinking-block__trigger').trigger('click');
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(false);
await wrapper.setProps({
items: [
thinkingMessage({
content: '分析问题并继续接收思考分片',
expanded: true,
}),
],
});
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(false);
await wrapper.get('.chat-thinking-block__trigger').trigger('click');
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(true);
await wrapper.setProps({
items: [
thinkingMessage({
content: '分析问题并继续接收正文分片',
expanded: false,
thinkingStatus: 'end',
}),
],
});
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(true);
});
it('uses the stream default when the user has not toggled thinking', async () => {
const wrapper = mount(ChatTimeline, {
props: {
items: [thinkingMessage()],
},
});
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(true);
await wrapper.setProps({
items: [
thinkingMessage({
expanded: false,
messageStatus: 'done',
thinkingStatus: 'end',
}),
],
});
expect(wrapper.find('.chat-thinking-block__body').exists()).toBe(false);
});
});