feat(vscode-ide-companion): 新增上下文附件管理功能

- 新增 ContextAttachmentManager 管理上下文附件
- 新增 ContextPills 组件用于显示上下文标签
- 支持文件、符号、选区等多种上下文类型

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yiliang114
2025-11-21 01:52:29 +08:00
parent 328924f578
commit b82ef5b73f
3 changed files with 272 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Context attachment types
* Based on vscode-copilot-chat implementation
*/
export interface ContextAttachment {
id: string;
type: 'file' | 'symbol' | 'selection' | 'variable';
name: string;
value: string | { uri: string; range?: { start: number; end: number } };
icon?: string;
}
/**
* Manages context attachments for the chat
* Similar to ChatContextAttachments in vscode-copilot-chat
*/
export class ContextAttachmentManager {
private attachments: Map<string, ContextAttachment> = new Map();
private listeners: Array<(attachments: ContextAttachment[]) => void> = [];
/**
* Add a context attachment
*/
addAttachment(attachment: ContextAttachment): void {
this.attachments.set(attachment.id, attachment);
this.notifyListeners();
}
/**
* Remove a context attachment
*/
removeAttachment(id: string): void {
this.attachments.delete(id);
this.notifyListeners();
}
/**
* Get all attachments
*/
getAttachments(): ContextAttachment[] {
return Array.from(this.attachments.values());
}
/**
* Check if an attachment exists
*/
hasAttachment(id: string): boolean {
return this.attachments.has(id);
}
/**
* Clear all attachments
*/
clearAttachments(): void {
this.attachments.clear();
this.notifyListeners();
}
/**
* Subscribe to attachment changes
*/
subscribe(listener: (attachments: ContextAttachment[]) => void): () => void {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
/**
* Notify all listeners of changes
*/
private notifyListeners(): void {
const attachments = this.getAttachments();
this.listeners.forEach((listener) => listener(attachments));
}
/**
* Get context for message sending
*/
getContextForMessage(): Array<Record<string, unknown>> {
return this.getAttachments().map((att) => ({
id: att.id,
type: att.type,
name: att.name,
value: att.value,
}));
}
}

View File

@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
.context-pills-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px;
background: var(--vscode-input-background, #3c3c3c);
border-bottom: 1px solid var(--vscode-input-border, #454545);
}
.context-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px 4px 6px;
background: var(--vscode-badge-background, #4d4d4d);
color: var(--vscode-badge-foreground, #ffffff);
border-radius: 12px;
font-size: 12px;
max-width: 200px;
transition: background-color 0.1s ease;
}
.context-pill:hover {
background: var(--vscode-badge-background, #5a5a5a);
}
.context-pill-icon {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
}
.context-pill-icon svg {
width: 14px;
height: 14px;
}
.context-pill-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.context-pill-remove {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.1s ease;
flex-shrink: 0;
}
.context-pill-remove:hover {
opacity: 1;
}
.context-pill-remove svg {
width: 12px;
height: 12px;
}

View File

@@ -0,0 +1,96 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import type { ContextAttachment } from '../ContextAttachmentManager.js';
import './ContextPills.css';
interface ContextPillsProps {
attachments: ContextAttachment[];
onRemove: (id: string) => void;
}
/**
* Display attached context as pills/chips
* Similar to ChatContextAttachments UI in vscode-copilot-chat
*/
export const ContextPills: React.FC<ContextPillsProps> = ({
attachments,
onRemove,
}) => {
if (attachments.length === 0) {
return null;
}
const getIcon = (type: string) => {
switch (type) {
case 'file':
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M5 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Z" />
</svg>
);
case 'symbol':
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 1a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 7.293V1.5A.5.5 0 0 1 8 1Z" />
</svg>
);
case 'selection':
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Z" />
</svg>
);
default:
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 2a.5.5 0 0 1 .5.5V5h2.5a.5.5 0 0 1 0 1H8.5v2.5a.5.5 0 0 1-1 0V6H5a.5.5 0 0 1 0-1h2.5V2.5A.5.5 0 0 1 8 2Z" />
</svg>
);
}
};
return (
<div className="context-pills-container">
{attachments.map((attachment) => (
<div key={attachment.id} className="context-pill">
<div className="context-pill-icon">{getIcon(attachment.type)}</div>
<div className="context-pill-label">{attachment.name}</div>
<button
className="context-pill-remove"
onClick={() => onRemove(attachment.id)}
aria-label="Remove attachment"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708Z" />
</svg>
</button>
</div>
))}
</div>
);
};