feat(vscode): 重构 Qwen 交互模型并优化权限请求 UI

- 重构 QwenAgentManager 类,支持处理多种类型的消息更新
- 改进权限请求界面,增加详细信息展示和选项选择功能
- 新增工具调用卡片组件,用于展示工具调用相关信息
- 优化消息流处理逻辑,支持不同类型的内容块
- 调整会话切换和新会话创建的处理方式
This commit is contained in:
yiliang114
2025-11-18 01:00:25 +08:00
parent eeeb1d490a
commit 28892996b3
8 changed files with 1245 additions and 162 deletions

View File

@@ -6,12 +6,48 @@
import React, { useState, useEffect, useRef } from 'react';
import { useVSCode } from './hooks/useVSCode.js';
import type { ChatMessage } from '../agents/QwenAgentManager.js';
import type { Conversation } from '../storage/ConversationStore.js';
import {
PermissionRequest,
type PermissionOption,
type ToolCall as PermissionToolCall,
} from './components/PermissionRequest.js';
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
interface ToolCallUpdate {
type: 'tool_call' | 'tool_call_update';
toolCallId: string;
kind?: string;
title?: string;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
rawInput?: unknown;
content?: Array<{
type: 'content' | 'diff';
content?: {
type: string;
text?: string;
[key: string]: unknown;
};
path?: string;
oldText?: string | null;
newText?: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
}
interface TextMessage {
role: 'user' | 'assistant' | 'thinking';
content: string;
timestamp: number;
}
export const App: React.FC = () => {
const vscode = useVSCode();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [messages, setMessages] = useState<TextMessage[]>([]);
const [inputText, setInputText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [currentStreamContent, setCurrentStreamContent] = useState('');
@@ -20,18 +56,20 @@ export const App: React.FC = () => {
>([]);
const [showSessionSelector, setShowSessionSelector] = useState(false);
const [permissionRequest, setPermissionRequest] = useState<{
options: Array<{ name: string; kind: string; optionId: string }>;
toolCall: { title?: string };
options: PermissionOption[];
toolCall: PermissionToolCall;
} | null>(null);
const [toolCalls, setToolCalls] = useState<Map<string, ToolCallData>>(
new Map(),
);
const messagesEndRef = useRef<HTMLDivElement>(null);
const handlePermissionRequest = React.useCallback(
(request: {
options: Array<{ name: string; kind: string; optionId: string }>;
toolCall: { title?: string };
options: PermissionOption[];
toolCall: PermissionToolCall;
}) => {
console.log('[WebView] Permission request received:', request);
// Show custom modal instead of window.confirm()
setPermissionRequest(request);
},
[],
@@ -49,6 +87,56 @@ export const App: React.FC = () => {
[vscode],
);
const handleToolCallUpdate = React.useCallback((update: ToolCallUpdate) => {
setToolCalls((prev) => {
const newMap = new Map(prev);
const existing = newMap.get(update.toolCallId);
if (update.type === 'tool_call') {
// New tool call - cast content to proper type
const content = update.content?.map((item) => ({
type: item.type as 'content' | 'diff',
content: item.content,
path: item.path,
oldText: item.oldText,
newText: item.newText,
}));
newMap.set(update.toolCallId, {
toolCallId: update.toolCallId,
kind: update.kind || 'other',
title: update.title || 'Tool Call',
status: update.status || 'pending',
rawInput: update.rawInput as string | object | undefined,
content,
locations: update.locations,
});
} else if (update.type === 'tool_call_update' && existing) {
// Update existing tool call
const updatedContent = update.content
? update.content.map((item) => ({
type: item.type as 'content' | 'diff',
content: item.content,
path: item.path,
oldText: item.oldText,
newText: item.newText,
}))
: undefined;
newMap.set(update.toolCallId, {
...existing,
...(update.kind && { kind: update.kind }),
...(update.title && { title: update.title }),
...(update.status && { status: update.status }),
...(updatedContent && { content: updatedContent }),
...(update.locations && { locations: update.locations }),
});
}
return newMap;
});
}, []);
useEffect(() => {
// Listen for messages from extension
const handleMessage = (event: MessageEvent) => {
@@ -62,7 +150,7 @@ export const App: React.FC = () => {
}
case 'message': {
const newMessage = message.data as ChatMessage;
const newMessage = message.data as TextMessage;
setMessages((prev) => [...prev, newMessage]);
break;
}
@@ -72,14 +160,21 @@ export const App: React.FC = () => {
setCurrentStreamContent('');
break;
case 'streamChunk':
setCurrentStreamContent((prev) => prev + message.data.chunk);
case 'streamChunk': {
const chunkData = message.data;
if (chunkData.role === 'thinking') {
// Handle thinking chunks separately if needed
setCurrentStreamContent((prev) => prev + chunkData.chunk);
} else {
setCurrentStreamContent((prev) => prev + chunkData.chunk);
}
break;
}
case 'streamEnd':
// Finalize the streamed message
if (currentStreamContent) {
const assistantMessage: ChatMessage = {
const assistantMessage: TextMessage = {
role: 'assistant',
content: currentStreamContent,
timestamp: Date.now(),
@@ -100,6 +195,12 @@ export const App: React.FC = () => {
handlePermissionRequest(message.data);
break;
case 'toolCall':
case 'toolCallUpdate':
// Handle tool call updates
handleToolCallUpdate(message.data);
break;
case 'qwenSessionList':
setQwenSessions(message.data.sessions || []);
break;
@@ -113,11 +214,13 @@ export const App: React.FC = () => {
setMessages([]);
}
setCurrentStreamContent('');
setToolCalls(new Map());
break;
case 'conversationCleared':
setMessages([]);
setCurrentStreamContent('');
setToolCalls(new Map());
break;
default:
@@ -127,7 +230,7 @@ export const App: React.FC = () => {
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [currentStreamContent, handlePermissionRequest]);
}, [currentStreamContent, handlePermissionRequest, handleToolCallUpdate]);
useEffect(() => {
// Auto-scroll to bottom when messages change
@@ -250,43 +353,18 @@ export const App: React.FC = () => {
</div>
))}
{/* Claude-style Inline Permission Request */}
{/* Tool Calls */}
{Array.from(toolCalls.values()).map((toolCall) => (
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
))}
{/* Permission Request */}
{permissionRequest && (
<div className="permission-request-inline">
<div className="permission-card">
<div className="permission-card-header">
<div className="permission-icon-wrapper">
<span className="permission-icon">🔧</span>
</div>
<div className="permission-info">
<div className="permission-tool-title">
{permissionRequest.toolCall.title || 'Tool Request'}
</div>
<div className="permission-subtitle">
Waiting for your approval
</div>
</div>
</div>
<div className="permission-actions-row">
{permissionRequest.options.map((option) => {
const isAllow = option.kind.includes('allow');
const isAlways = option.kind.includes('always');
return (
<button
key={option.optionId}
onClick={() => handlePermissionResponse(option.optionId)}
className={`permission-btn-inline ${isAllow ? 'allow' : 'reject'} ${isAlways ? 'always' : ''}`}
>
{isAlways && <span className="always-badge"></span>}
{option.name}
</button>
);
})}
</div>
</div>
</div>
<PermissionRequest
options={permissionRequest.options}
toolCall={permissionRequest.toolCall}
onResponse={handlePermissionResponse}
/>
)}
{isStreaming && currentStreamContent && (