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

@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import './ClaudeCompletionMenu.css';
import type { CompletionItem } from './CompletionMenu.js';
interface ClaudeCompletionMenuProps {
items: CompletionItem[];
onSelect: (item: CompletionItem) => void;
onClose: () => void;
title?: string;
selectedIndex?: number;
}
/**
* Claude Code-like anchored dropdown rendered above the input field.
* Keyboard: Up/Down to move, Enter to select, Esc to close.
*/
export const ClaudeCompletionMenu: React.FC<ClaudeCompletionMenuProps> = ({
items,
onSelect,
onClose,
title,
selectedIndex = 0,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [selected, setSelected] = useState(selectedIndex);
useEffect(() => setSelected(selectedIndex), [selectedIndex]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
onClose();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setSelected((prev) => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
event.preventDefault();
setSelected((prev) => Math.max(prev - 1, 0));
break;
case 'Enter':
event.preventDefault();
if (items[selected]) {
onSelect(items[selected]);
}
break;
case 'Escape':
event.preventDefault();
onClose();
break;
default:
break;
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
};
}, [items, selected, onSelect, onClose]);
useEffect(() => {
const selectedEl = containerRef.current?.querySelector(
`[data-index="${selected}"]`,
);
if (selectedEl) {
selectedEl.scrollIntoView({ block: 'nearest' });
}
}, [selected]);
if (!items.length) {
return null;
}
return (
<div ref={containerRef} className="hi" role="menu">
<div className="spacer-4px" />
<div className="xi">
{title && <div className="vi">{title}</div>}
{items.map((item, index) => {
const selectedCls = index === selected ? 'jo' : '';
return (
<div
key={item.id}
data-index={index}
className={`wi ${selectedCls}`}
onClick={() => onSelect(item)}
onMouseEnter={() => setSelected(index)}
role="menuitem"
>
<div className="ki">
{item.icon && <span className="Ii">{item.icon}</span>}
<span className="Lo">{item.label}</span>
{item.description && (
<span className="Mo" title={item.description}>
{item.description}
</span>
)}
</div>
</div>
);
})}
</div>
</div>
);
};