mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
feat(vscode-ide-companion): 改进消息排序和显示逻辑
- 添加时间戳支持,确保消息按时间顺序排列 - 更新工具调用处理逻辑,自动添加和保留时间戳 - 修改消息渲染逻辑,将所有类型的消息合并排序后统一渲染 - 优化完成的工具调用显示,修复显示顺序问题 - 调整进行中的工具调用显示,统一到消息流中展示 - 移除重复的计划展示逻辑,避免最新块重复出现 - 重构消息处理和渲染代码,提高可维护性
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user