mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat(vscode): 重构 Qwen 交互模型并优化权限请求 UI
- 重构 QwenAgentManager 类,支持处理多种类型的消息更新 - 改进权限请求界面,增加详细信息展示和选项选择功能 - 新增工具调用卡片组件,用于展示工具调用相关信息 - 优化消息流处理逻辑,支持不同类型的内容块 - 调整会话切换和新会话创建的处理方式
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface PermissionOption {
|
||||
name: string;
|
||||
kind: string;
|
||||
optionId: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
title?: string;
|
||||
kind?: string;
|
||||
toolCallId?: string;
|
||||
rawInput?: {
|
||||
command?: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
content?: Array<{
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
locations?: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface PermissionRequestProps {
|
||||
options: PermissionOption[];
|
||||
toolCall: ToolCall;
|
||||
onResponse: (optionId: string) => void;
|
||||
}
|
||||
|
||||
export const PermissionRequest: React.FC<PermissionRequestProps> = ({
|
||||
options,
|
||||
toolCall,
|
||||
onResponse,
|
||||
}) => {
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [isResponding, setIsResponding] = useState(false);
|
||||
const [hasResponded, setHasResponded] = useState(false);
|
||||
|
||||
const getToolInfo = () => {
|
||||
if (!toolCall) {
|
||||
return {
|
||||
title: 'Permission Request',
|
||||
description: 'Agent is requesting permission',
|
||||
icon: '🔐',
|
||||
};
|
||||
}
|
||||
|
||||
const displayTitle =
|
||||
toolCall.title || toolCall.rawInput?.description || 'Permission Request';
|
||||
|
||||
const kindIcons: Record<string, string> = {
|
||||
edit: '✏️',
|
||||
read: '📖',
|
||||
fetch: '🌐',
|
||||
execute: '⚡',
|
||||
delete: '🗑️',
|
||||
move: '📦',
|
||||
search: '🔍',
|
||||
think: '💭',
|
||||
other: '🔧',
|
||||
};
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
icon: kindIcons[toolCall.kind || 'other'] || '🔧',
|
||||
};
|
||||
};
|
||||
|
||||
const { title, icon } = getToolInfo();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (hasResponded || !selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResponding(true);
|
||||
try {
|
||||
await onResponse(selected);
|
||||
setHasResponded(true);
|
||||
} catch (error) {
|
||||
console.error('Error confirming permission:', error);
|
||||
} finally {
|
||||
setIsResponding(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!toolCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="permission-request-card">
|
||||
<div className="permission-card-body">
|
||||
{/* Header with icon and title */}
|
||||
<div className="permission-header">
|
||||
<div className="permission-icon-wrapper">
|
||||
<span className="permission-icon">{icon}</span>
|
||||
</div>
|
||||
<div className="permission-info">
|
||||
<div className="permission-title">{title}</div>
|
||||
<div className="permission-subtitle">Waiting for your approval</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show command if available */}
|
||||
{(toolCall.rawInput?.command || toolCall.title) && (
|
||||
<div className="permission-command-section">
|
||||
<div className="permission-command-label">Command</div>
|
||||
<code className="permission-command-code">
|
||||
{toolCall.rawInput?.command || toolCall.title}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show file locations if available */}
|
||||
{toolCall.locations && toolCall.locations.length > 0 && (
|
||||
<div className="permission-locations-section">
|
||||
<div className="permission-locations-label">Affected Files</div>
|
||||
{toolCall.locations.map((location, index) => (
|
||||
<div key={index} className="permission-location-item">
|
||||
<span className="permission-location-icon">📄</span>
|
||||
<span className="permission-location-path">
|
||||
{location.path}
|
||||
</span>
|
||||
{location.line !== null && location.line !== undefined && (
|
||||
<span className="permission-location-line">
|
||||
::{location.line}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Options */}
|
||||
{!hasResponded && (
|
||||
<div className="permission-options-section">
|
||||
<div className="permission-options-label">Choose an action:</div>
|
||||
<div className="permission-options-list">
|
||||
{options && options.length > 0 ? (
|
||||
options.map((option) => {
|
||||
const isSelected = selected === option.optionId;
|
||||
const isAllow = option.kind.includes('allow');
|
||||
const isAlways = option.kind.includes('always');
|
||||
|
||||
return (
|
||||
<label
|
||||
key={option.optionId}
|
||||
className={`permission-option ${isSelected ? 'selected' : ''} ${
|
||||
isAllow ? 'allow' : 'reject'
|
||||
} ${isAlways ? 'always' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="permission"
|
||||
value={option.optionId}
|
||||
checked={isSelected}
|
||||
onChange={() => setSelected(option.optionId)}
|
||||
className="permission-radio"
|
||||
/>
|
||||
<span className="permission-option-content">
|
||||
{isAlways && (
|
||||
<span className="permission-always-badge">⚡</span>
|
||||
)}
|
||||
{option.name}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="permission-no-options">
|
||||
No options available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="permission-actions">
|
||||
<button
|
||||
className="permission-confirm-button"
|
||||
disabled={!selected || isResponding}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{isResponding ? 'Processing...' : 'Confirm'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success message */}
|
||||
{hasResponded && (
|
||||
<div className="permission-success">
|
||||
<span className="permission-success-icon">✓</span>
|
||||
<span className="permission-success-text">
|
||||
Response sent successfully
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export interface ToolCallContent {
|
||||
type: 'content' | 'diff';
|
||||
// For content type
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// For diff type
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
export interface ToolCallData {
|
||||
toolCallId: string;
|
||||
kind: string;
|
||||
title: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return { className: 'status-pending', text: 'Pending', icon: '⏳' };
|
||||
case 'in_progress':
|
||||
return {
|
||||
className: 'status-in-progress',
|
||||
text: 'In Progress',
|
||||
icon: '🔄',
|
||||
};
|
||||
case 'completed':
|
||||
return { className: 'status-completed', text: 'Completed', icon: '✓' };
|
||||
case 'failed':
|
||||
return { className: 'status-failed', text: 'Failed', icon: '✗' };
|
||||
default:
|
||||
return { className: 'status-unknown', text: status, icon: '•' };
|
||||
}
|
||||
};
|
||||
|
||||
const { className, text, icon } = getStatusInfo();
|
||||
return (
|
||||
<span className={`tool-call-status ${className}`}>
|
||||
<span className="status-icon">{icon}</span>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ContentView: React.FC<{ content: ToolCallContent }> = ({ content }) => {
|
||||
// Handle diff type
|
||||
if (content.type === 'diff') {
|
||||
const fileName =
|
||||
content.path?.split(/[/\\]/).pop() || content.path || 'Unknown file';
|
||||
const oldText = content.oldText || '';
|
||||
const newText = content.newText || '';
|
||||
|
||||
return (
|
||||
<div className="tool-call-diff">
|
||||
<div className="diff-header">
|
||||
<span className="diff-icon">📝</span>
|
||||
<span className="diff-filename">{fileName}</span>
|
||||
</div>
|
||||
<div className="diff-content">
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">Before</div>
|
||||
<pre className="diff-code">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
<div className="diff-arrow">→</div>
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">After</div>
|
||||
<pre className="diff-code">{newText || '(empty)'}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle content type with text
|
||||
if (content.type === 'content' && content.content?.text) {
|
||||
return (
|
||||
<div className="tool-call-content">
|
||||
<div className="content-text">{content.content.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getKindDisplayName = (kind: string): { name: string; icon: string } => {
|
||||
const kindMap: Record<string, { name: string; icon: string }> = {
|
||||
edit: { name: 'File Edit', icon: '✏️' },
|
||||
read: { name: 'File Read', icon: '📖' },
|
||||
execute: { name: 'Shell Command', icon: '⚡' },
|
||||
fetch: { name: 'Web Fetch', icon: '🌐' },
|
||||
delete: { name: 'Delete', icon: '🗑️' },
|
||||
move: { name: 'Move/Rename', icon: '📦' },
|
||||
search: { name: 'Search', icon: '🔍' },
|
||||
think: { name: 'Thinking', icon: '💭' },
|
||||
other: { name: 'Other', icon: '🔧' },
|
||||
};
|
||||
|
||||
return kindMap[kind] || { name: kind, icon: '🔧' };
|
||||
};
|
||||
|
||||
const formatRawInput = (rawInput: string | object | undefined): string => {
|
||||
if (rawInput === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof rawInput === 'string') {
|
||||
return rawInput;
|
||||
}
|
||||
return JSON.stringify(rawInput, null, 2);
|
||||
};
|
||||
|
||||
export const ToolCall: React.FC<ToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations, toolCallId } =
|
||||
toolCall;
|
||||
const kindInfo: { name: string; icon: string } = getKindDisplayName(kind);
|
||||
|
||||
return (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-header">
|
||||
<span className="tool-call-kind-icon">{kindInfo.icon}</span>
|
||||
<span className="tool-call-title">{title || kindInfo.name}</span>
|
||||
<StatusTag status={status} />
|
||||
</div>
|
||||
|
||||
{/* Show raw input if available */}
|
||||
{rawInput !== undefined && rawInput !== null ? (
|
||||
<div className="tool-call-raw-input">
|
||||
<div className="raw-input-label">Input</div>
|
||||
<pre className="raw-input-content">{formatRawInput(rawInput)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Show locations if available */}
|
||||
{locations && locations.length > 0 && (
|
||||
<div className="tool-call-locations">
|
||||
<div className="locations-label">Files</div>
|
||||
{locations.map((location, index) => (
|
||||
<div key={index} className="location-item">
|
||||
<span className="location-icon">📄</span>
|
||||
<span className="location-path">{location.path}</span>
|
||||
{location.line !== null && location.line !== undefined && (
|
||||
<span className="location-line">:{location.line}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show content if available */}
|
||||
{content && content.length > 0 && (
|
||||
<div className="tool-call-content-list">
|
||||
{content.map((item, index) => (
|
||||
<ContentView key={index} content={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tool-call-footer">
|
||||
<span className="tool-call-id">
|
||||
ID: {toolCallId.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user