mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode): 重构 Qwen 交互模型并优化权限请求 UI
- 重构 QwenAgentManager 类,支持处理多种类型的消息更新 - 改进权限请求界面,增加详细信息展示和选项选择功能 - 新增工具调用卡片组件,用于展示工具调用相关信息 - 优化消息流处理逻辑,支持不同类型的内容块 - 调整会话切换和新会话创建的处理方式
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user