mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat(vscode-ide-companion): 增加代码编辑功能和文件操作支持
- 实现了与 Claude Code 类似的代码编辑功能 - 添加了文件打开、保存等操作的支持 - 优化了消息显示,增加了代码高亮和文件路径点击功能 - 改进了用户界面,增加了编辑模式切换和思考模式功能
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
||||
import { PermissionDrawer } from './components/PermissionDrawer.js';
|
||||
import { ToolCall, type ToolCallData } from './components/ToolCall.js';
|
||||
import { EmptyState } from './components/EmptyState.js';
|
||||
import { PlanDisplay, type PlanEntry } from './components/PlanDisplay.js';
|
||||
import { MessageContent } from './components/MessageContent.js';
|
||||
|
||||
interface ToolCallUpdate {
|
||||
type: 'tool_call' | 'tool_call_update';
|
||||
@@ -185,6 +187,8 @@ const getRandomLoadingMessage = () =>
|
||||
Math.floor(Math.random() * WITTY_LOADING_PHRASES.length)
|
||||
];
|
||||
|
||||
type EditMode = 'ask' | 'auto' | 'plan';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const vscode = useVSCode();
|
||||
const [messages, setMessages] = useState<TextMessage[]>([]);
|
||||
@@ -208,10 +212,14 @@ export const App: React.FC = () => {
|
||||
const [toolCalls, setToolCalls] = useState<Map<string, ToolCallData>>(
|
||||
new Map(),
|
||||
);
|
||||
const [planEntries, setPlanEntries] = useState<PlanEntry[]>([]);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputFieldRef = useRef<HTMLDivElement>(null);
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
const currentStreamContentRef = useRef<string>('');
|
||||
const [editMode, setEditMode] = useState<EditMode>('ask');
|
||||
const [thinkingEnabled, setThinkingEnabled] = useState(false);
|
||||
const [activeFileName, setActiveFileName] = useState<string | null>(null);
|
||||
|
||||
const handlePermissionRequest = React.useCallback(
|
||||
(request: {
|
||||
@@ -376,6 +384,14 @@ export const App: React.FC = () => {
|
||||
handlePermissionRequest(message.data);
|
||||
break;
|
||||
|
||||
case 'plan':
|
||||
// Update plan entries
|
||||
console.log('[App] Plan received:', message.data);
|
||||
if (message.data.entries && Array.isArray(message.data.entries)) {
|
||||
setPlanEntries(message.data.entries as PlanEntry[]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'toolCall':
|
||||
case 'toolCallUpdate':
|
||||
// Handle tool call updates
|
||||
@@ -448,6 +464,7 @@ export const App: React.FC = () => {
|
||||
}
|
||||
setCurrentStreamContent('');
|
||||
setToolCalls(new Map());
|
||||
setPlanEntries([]); // Clear plan entries when switching sessions
|
||||
break;
|
||||
|
||||
case 'conversationCleared':
|
||||
@@ -456,6 +473,13 @@ export const App: React.FC = () => {
|
||||
setToolCalls(new Map());
|
||||
break;
|
||||
|
||||
case 'activeEditorChanged': {
|
||||
// 从扩展接收当前激活编辑器的文件名
|
||||
const fileName = message.data?.fileName as string | null;
|
||||
setActiveFileName(fileName);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -475,6 +499,90 @@ export const App: React.FC = () => {
|
||||
vscode.postMessage({ type: 'getQwenSessions', data: {} });
|
||||
}, [vscode]);
|
||||
|
||||
// Request current active editor on component mount
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: 'getActiveEditor', data: {} });
|
||||
}, [vscode]);
|
||||
|
||||
// Toggle edit mode: ask → auto → plan → ask
|
||||
const handleToggleEditMode = () => {
|
||||
setEditMode((prev) => {
|
||||
if (prev === 'ask') {
|
||||
return 'auto';
|
||||
}
|
||||
if (prev === 'auto') {
|
||||
return 'plan';
|
||||
}
|
||||
return 'ask';
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle thinking on/off
|
||||
const handleToggleThinking = () => {
|
||||
setThinkingEnabled((prev) => !prev);
|
||||
};
|
||||
|
||||
// Get edit mode display info
|
||||
const getEditModeInfo = () => {
|
||||
switch (editMode) {
|
||||
case 'ask':
|
||||
return {
|
||||
text: 'Ask before edits',
|
||||
title: 'Qwen will ask before each edit. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'auto':
|
||||
return {
|
||||
text: 'Edit automatically',
|
||||
title: 'Qwen will edit files automatically. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
case 'plan':
|
||||
return {
|
||||
text: 'Plan mode',
|
||||
title: 'Qwen will plan before executing. Click to switch modes.',
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1ZM10.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1Z"></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Unknown mode',
|
||||
title: 'Unknown edit mode',
|
||||
icon: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -511,6 +619,8 @@ export const App: React.FC = () => {
|
||||
// Clear messages in UI
|
||||
setMessages([]);
|
||||
setCurrentStreamContent('');
|
||||
setPlanEntries([]); // Clear plan entries
|
||||
setToolCalls(new Map()); // Clear tool calls
|
||||
};
|
||||
|
||||
// Time ago formatter (matching Claude Code)
|
||||
@@ -624,7 +734,11 @@ export const App: React.FC = () => {
|
||||
};
|
||||
|
||||
// Check if there are any messages or active content
|
||||
const hasContent = messages.length > 0 || isStreaming || toolCalls.size > 0;
|
||||
const hasContent =
|
||||
messages.length > 0 ||
|
||||
isStreaming ||
|
||||
toolCalls.size > 0 ||
|
||||
planEntries.length > 0;
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
@@ -792,7 +906,15 @@ export const App: React.FC = () => {
|
||||
<span className="thinking-dot"></span>
|
||||
</span>
|
||||
)}
|
||||
{msg.content}
|
||||
<MessageContent
|
||||
content={msg.content}
|
||||
onFileClick={(path) => {
|
||||
vscode.postMessage({
|
||||
type: 'openFile',
|
||||
data: { path },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="message-timestamp">
|
||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||
@@ -806,6 +928,9 @@ export const App: React.FC = () => {
|
||||
<ToolCall key={toolCall.toolCallId} toolCall={toolCall} />
|
||||
))}
|
||||
|
||||
{/* Plan Display - shows task list when available */}
|
||||
{planEntries.length > 0 && <PlanDisplay entries={planEntries} />}
|
||||
|
||||
{/* Loading/Waiting Message - in message list */}
|
||||
{isWaitingForResponse && loadingMessage && (
|
||||
<div className="message assistant waiting-message">
|
||||
@@ -822,7 +947,17 @@ export const App: React.FC = () => {
|
||||
|
||||
{isStreaming && currentStreamContent && (
|
||||
<div className="message assistant streaming">
|
||||
<div className="message-content">{currentStreamContent}</div>
|
||||
<div className="message-content">
|
||||
<MessageContent
|
||||
content={currentStreamContent}
|
||||
onFileClick={(path) => {
|
||||
vscode.postMessage({
|
||||
type: 'openFile',
|
||||
data: { path },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="streaming-indicator">●</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -884,6 +1019,7 @@ export const App: React.FC = () => {
|
||||
<div className="input-form-container">
|
||||
<div className="input-form-wrapper">
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<div className="input-form-background"></div>
|
||||
<div className="input-banner"></div>
|
||||
<div className="input-wrapper">
|
||||
<div
|
||||
@@ -911,27 +1047,18 @@ export const App: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
className="action-button edit-mode-button"
|
||||
title="Qwen will ask before each edit. Click to switch modes."
|
||||
title={getEditModeInfo().title}
|
||||
onClick={handleToggleEditMode}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Ask before edits</span>
|
||||
{getEditModeInfo().icon}
|
||||
<span>{getEditModeInfo().text}</span>
|
||||
</button>
|
||||
<div className="action-divider"></div>
|
||||
<button
|
||||
type="button"
|
||||
className="action-icon-button thinking-button"
|
||||
title="Thinking off"
|
||||
className={`action-icon-button thinking-button ${thinkingEnabled ? 'active' : ''}`}
|
||||
title={thinkingEnabled ? 'Thinking on' : 'Thinking off'}
|
||||
onClick={handleToggleThinking}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
@@ -968,6 +1095,15 @@ export const App: React.FC = () => {
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="input-actions-spacer"></div>
|
||||
{activeFileName && (
|
||||
<span
|
||||
className="active-file-indicator"
|
||||
title={`Showing Qwen Code your current file selection: ${activeFileName}`}
|
||||
>
|
||||
{activeFileName}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button-icon"
|
||||
|
||||
Reference in New Issue
Block a user