feat(vscode-ide-companion): 改进消息排序和显示逻辑

- 添加时间戳支持,确保消息按时间顺序排列
- 更新工具调用处理逻辑,自动添加和保留时间戳
- 修改消息渲染逻辑,将所有类型的消息合并排序后统一渲染
- 优化完成的工具调用显示,修复显示顺序问题
- 调整进行中的工具调用显示,统一到消息流中展示
- 移除重复的计划展示逻辑,避免最新块重复出现
- 重构消息处理和渲染代码,提高可维护性
This commit is contained in:
yiliang114
2025-11-28 09:55:06 +08:00
parent dc340daf8b
commit 9cc48f12da
44 changed files with 2445 additions and 767 deletions

View File

@@ -32,6 +32,11 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
onFileClick,
status = 'default',
}) => {
// 空内容直接不渲染,避免只显示 ::before 的圆点导致观感不佳
if (!content || content.trim().length === 0) {
return null;
}
// Map status to CSS class (only for ::before pseudo-element)
const getStatusClass = () => {
switch (status) {

View File

@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { ToolCallData } from '../toolcalls/shared/types.js';
import { hasToolCallOutput } from '../toolcalls/shared/utils.js';
describe('Message Ordering', () => {
it('should correctly identify tool calls with output', () => {
// Test failed tool call (should show)
const failedToolCall: ToolCallData = {
toolCallId: 'test-1',
kind: 'read',
title: 'Read file',
status: 'failed',
timestamp: 1000,
};
expect(hasToolCallOutput(failedToolCall)).toBe(true);
// Test execute tool call with title (should show)
const executeToolCall: ToolCallData = {
toolCallId: 'test-2',
kind: 'execute',
title: 'ls -la',
status: 'completed',
timestamp: 2000,
};
expect(hasToolCallOutput(executeToolCall)).toBe(true);
// Test tool call with content (should show)
const contentToolCall: ToolCallData = {
toolCallId: 'test-3',
kind: 'read',
title: 'Read file',
status: 'completed',
content: [
{
type: 'content',
content: {
type: 'text',
text: 'File content',
},
},
],
timestamp: 3000,
};
expect(hasToolCallOutput(contentToolCall)).toBe(true);
// Test tool call with locations (should show)
const locationToolCall: ToolCallData = {
toolCallId: 'test-4',
kind: 'read',
title: 'Read file',
status: 'completed',
locations: [
{
path: '/path/to/file.txt',
},
],
timestamp: 4000,
};
expect(hasToolCallOutput(locationToolCall)).toBe(true);
// Test tool call with title (should show)
const titleToolCall: ToolCallData = {
toolCallId: 'test-5',
kind: 'generic',
title: 'Generic tool call',
status: 'completed',
timestamp: 5000,
};
expect(hasToolCallOutput(titleToolCall)).toBe(true);
// Test tool call without output (should not show)
const noOutputToolCall: ToolCallData = {
toolCallId: 'test-6',
kind: 'generic',
title: '',
status: 'completed',
timestamp: 6000,
};
expect(hasToolCallOutput(noOutputToolCall)).toBe(false);
});
});

View File

@@ -29,7 +29,9 @@ export const UserMessage: React.FC<UserMessageProps> = ({
}) => {
// Generate display text for file context
const getFileContextDisplay = () => {
if (!fileContext) return null;
if (!fileContext) {
return null;
}
const { fileName, startLine, endLine } = fileContext;
if (startLine && endLine) {
return startLine === endLine

View File

@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/* Subtle shimmering highlight across the loading text */
@keyframes waitingMessageShimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.loading-text-shimmer {
/* Use the theme foreground as the base color, with a moving light band */
background-image: linear-gradient(
90deg,
var(--app-secondary-foreground) 0%,
var(--app-secondary-foreground) 40%,
rgba(255, 255, 255, 0.95) 50%,
var(--app-secondary-foreground) 60%,
var(--app-secondary-foreground) 100%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent; /* text color comes from the gradient */
animation: waitingMessageShimmer 1.6s linear infinite;
}

View File

@@ -5,27 +5,84 @@
*/
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import './AssistantMessage.css';
import './WaitingMessage.css';
import { WITTY_LOADING_PHRASES } from '../../../constants/loadingMessages.js';
interface WaitingMessageProps {
loadingMessage: string;
}
// Rotate message every few seconds while waiting
const ROTATE_INTERVAL_MS = 3000; // rotate every 3s per request
export const WaitingMessage: React.FC<WaitingMessageProps> = ({
loadingMessage,
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85 animate-[fadeIn_0.2s_ease-in]">
<div className="bg-transparent border-0 py-2 flex items-center gap-2">
<span className="inline-flex items-center gap-1 mr-0">
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.2s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.4s]"></span>
</span>
<span
className="opacity-70 italic"
style={{ color: 'var(--app-secondary-foreground)' }}
}) => {
// Build a phrase list that starts with the provided message (if any), then witty fallbacks
const phrases = useMemo(() => {
const set = new Set<string>();
const list: string[] = [];
if (loadingMessage && loadingMessage.trim()) {
list.push(loadingMessage);
set.add(loadingMessage);
}
for (const p of WITTY_LOADING_PHRASES) {
if (!set.has(p)) {
list.push(p);
}
}
return list;
}, [loadingMessage]);
const [index, setIndex] = useState(0);
// Reset to the first phrase whenever the incoming message changes
useEffect(() => {
setIndex(0);
}, [phrases]);
// Periodically rotate to a different phrase
useEffect(() => {
if (phrases.length <= 1) {
return;
}
const id = setInterval(() => {
setIndex((prev) => {
// pick a different random index to avoid immediate repeats
let next = Math.floor(Math.random() * phrases.length);
if (phrases.length > 1) {
let guard = 0;
while (next === prev && guard < 5) {
next = Math.floor(Math.random() * phrases.length);
guard++;
}
}
return next;
});
}, ROTATE_INTERVAL_MS);
return () => clearInterval(id);
}, [phrases]);
return (
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85 animate-[fadeIn_0.2s_ease-in]">
{/* Use the same left status icon (pseudo-element) style as assistant-message-container */}
<div
className="assistant-message-container assistant-message-loading"
style={{
width: '100%',
alignItems: 'flex-start',
paddingLeft: '30px', // reserve space for ::before bullet
position: 'relative',
paddingTop: '8px',
paddingBottom: '8px',
}}
>
{loadingMessage}
</span>
<span className="opacity-70 italic loading-text-shimmer">
{phrases[index]}
</span>
</div>
</div>
</div>
);
);
};

View File

@@ -9,3 +9,4 @@ export { AssistantMessage } from './AssistantMessage.js';
export { ThinkingMessage } from './ThinkingMessage.js';
export { StreamingMessage } from './StreamingMessage.js';
export { WaitingMessage } from './WaitingMessage.js';
export { PlanDisplay } from '../PlanDisplay.js';