mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
21 KiB
21 KiB
Claude Code VSCode 扩展功能迁移可行性分析
一、概述
参考插件信息
- 名称: Claude Code for VS Code (Anthropic 官方)
- 版本: 2.0.43
- 状态: 已打包压缩 (extension.js 约 983KB)
目标插件信息
- 名称: Qwen Code VSCode IDE Companion
- 版本: 0.2.2
- 状态: 源代码可用,架构清晰
二、需求功能分析
用户期望迁移的功能
1. WebView CustomEditor 固定在编辑器右侧
描述: 将 webview 面板默认显示在代码编辑器的右侧(split view)
当前状态:
- Qwen 扩展: WebView 使用
vscode.ViewColumn.One(主编辑器列) - Claude 扩展: 支持多种布局方式
可行性: ✅ 完全可行
实现方案:
// 当前实现 (WebViewProvider.ts:77)
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code Chat',
vscode.ViewColumn.One, // ← 修改这里
{
/* ... */
},
);
// 建议修改为
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code Chat',
vscode.ViewColumn.Beside, // 在当前编辑器右侧打开
{
/* ... */
},
);
附加选项:
vscode.ViewColumn.Beside: 在当前活动编辑器旁边vscode.ViewColumn.Two: 固定在第二列- 可配置化,让用户选择默认位置
2. Webview 顶部组件布局
2.1 左侧: Session/Chat 选择器 (下拉菜单)
描述: 顶部左侧显示当前 session 名称,点击可下拉选择其他 session
当前状态:
- Qwen 扩展: 右侧有 "📋 Sessions" 按钮,点击打开模态框
- Claude 扩展: CSS 显示有
.E类(下拉按钮样式)
可行性: ✅ 完全可行
实现方案:
方案 A: 移动现有按钮到左侧
// App.tsx - 修改 header 布局
<div className="chat-header">
{/* 新增:左侧 session 选择器 */}
<div className="session-selector-dropdown">
<button
className="session-dropdown-button"
onClick={handleLoadQwenSessions}
>
<span className="session-icon">📋</span>
<span className="session-title">
{currentSessionTitle || 'Select Session'}
</span>
<span className="dropdown-icon">▼</span>
</button>
</div>
{/* 右侧新建 chat 按钮 */}
<div className="header-actions">
<button className="new-chat-button" onClick={handleNewQwenSession}>
➕
</button>
</div>
</div>
方案 B: 使用真正的下拉选择
// 使用 VSCode 原生选择器样式
<select
className="session-selector"
value={currentSessionId}
onChange={(e) => handleSwitchSession(e.target.value)}
>
{qwenSessions.map((session) => (
<option key={session.id} value={session.id}>
{getSessionTitle(session)}
</option>
))}
</select>
CSS 样式:
/* App.css - 添加以下样式 */
.chat-header {
display: flex;
justify-content: space-between; /* 两端对齐 */
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.session-selector-dropdown {
flex: 1;
min-width: 0;
}
.session-dropdown-button {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-foreground);
cursor: pointer;
max-width: 300px;
overflow: hidden;
}
.session-dropdown-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.session-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.dropdown-icon {
flex-shrink: 0;
opacity: 0.7;
}
2.2 右侧: 新建 Chat 按钮 (+ 号)
描述: 顶部右上角显示 + 号按钮,点击创建新 chat
当前状态:
- Qwen 扩展: 新建按钮在 session 选择器模态框内
- Claude 扩展: CSS 显示有
.j类(图标按钮样式)
可行性: ✅ 完全可行
实现方案:
// App.tsx - Header 右侧按钮
<div className="header-actions">
<button
className="icon-button new-chat-button"
onClick={handleNewQwenSession}
title="New Chat"
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M8 1v14M1 8h14" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
</div>
CSS 样式:
.header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.icon-button {
width: 24px;
height: 24px;
padding: 4px;
background: transparent;
border: none;
border-radius: 4px;
color: var(--vscode-foreground);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.icon-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.icon-button:active {
opacity: 0.7;
}
三、关键差异分析
Claude Code 扩展的特点
1. 多种打开方式
{
"commands": [
{ "command": "claude-vscode.editor.open", "title": "Open in New Tab" },
{ "command": "claude-vscode.sidebar.open", "title": "Open in Side Bar" },
{ "command": "claude-vscode.window.open", "title": "Open in New Window" }
]
}
迁移建议:
- 保留 Qwen 扩展的简单模式(单一命令)
- 可选:后续添加多种打开方式的支持
2. Sidebar View Container
{
"viewsContainers": {
"activitybar": [
{
"id": "claude-sidebar",
"title": "Claude",
"icon": "resources/claude-logo.svg"
}
]
},
"views": {
"claude-sidebar": [
{
"type": "webview",
"id": "claudeVSCodeSidebar",
"name": "Claude Code"
}
]
}
}
迁移建议:
- Qwen 扩展暂时不需要 Sidebar 容器
- 当前的 WebView Panel 方式更灵活
3. 配置项差异
| 配置项 | Claude Code | Qwen Code | 迁移建议 |
|---|---|---|---|
| 模型选择 | selectedModel |
qwen.model |
保持现有 |
| 环境变量 | environmentVariables |
无 | 可选添加 |
| 终端模式 | useTerminal |
无 | 不需要 |
| 权限模式 | initialPermissionMode |
无 | 不需要 |
四、实现步骤建议
阶段一: 基础布局调整 (1-2 天)
任务 1: 修改 WebView 打开位置
文件: src/WebViewProvider.ts
// 修改 show() 方法
async show(): Promise<void> {
if (this.panel) {
this.panel.reveal();
return;
}
this.panel = vscode.window.createWebviewPanel(
'qwenCode.chat',
'Qwen Code Chat',
{
viewColumn: vscode.ViewColumn.Beside, // 新增配置
preserveFocus: false
},
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(this.extensionUri, 'dist')
],
},
);
// ... 其余代码
}
任务 2: 重构 Header 组件
文件: src/webview/App.tsx
// 新增组件:ChatHeader
const ChatHeader: React.FC<{
currentSessionTitle: string;
onSessionsClick: () => void;
onNewChatClick: () => void;
}> = ({ currentSessionTitle, onSessionsClick, onNewChatClick }) => {
return (
<div className="chat-header">
<div className="session-selector-container">
<button className="session-dropdown-button" onClick={onSessionsClick}>
<span className="session-icon">📋</span>
<span className="session-title">
{currentSessionTitle || 'Select Session'}
</span>
<span className="dropdown-icon">▼</span>
</button>
</div>
<div className="header-actions">
<button
className="icon-button new-chat-button"
onClick={onNewChatClick}
title="New Chat (Ctrl+Shift+N)"
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path
d="M8 1v14M1 8h14"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
</div>
</div>
);
};
// 在 App 组件中使用
export const App: React.FC = () => {
const [currentSessionTitle, setCurrentSessionTitle] = useState<string>('');
// ... 其他状态
return (
<div className="chat-container">
<ChatHeader
currentSessionTitle={currentSessionTitle}
onSessionsClick={handleLoadQwenSessions}
onNewChatClick={handleNewQwenSession}
/>
{/* 其余组件 */}
</div>
);
};
任务 3: 更新样式
文件: src/webview/App.css
/* 替换现有的 .chat-header 样式 */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: var(--vscode-editor-background);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
min-height: 40px;
}
.session-selector-container {
flex: 1;
min-width: 0;
margin-right: 12px;
}
.session-dropdown-button {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-foreground);
cursor: pointer;
max-width: 100%;
overflow: hidden;
transition: background-color 0.2s;
}
.session-dropdown-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.session-dropdown-button:active {
background: var(--vscode-list-activeSelectionBackground);
}
.session-icon {
flex-shrink: 0;
font-size: 14px;
}
.session-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
font-size: 13px;
font-weight: 500;
}
.dropdown-icon {
flex-shrink: 0;
opacity: 0.7;
font-size: 10px;
transition: transform 0.2s;
}
.session-dropdown-button[aria-expanded='true'] .dropdown-icon {
transform: rotate(180deg);
}
.header-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.icon-button {
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
color: var(--vscode-foreground);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.icon-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.icon-button:active {
opacity: 0.7;
}
.new-chat-button svg {
width: 16px;
height: 16px;
}
/* 移除或修改原有的 .session-button 样式 */
.session-button {
/* 已移除,功能整合到 header */
}
阶段二: 功能增强 (2-3 天)
任务 4: 添加当前 Session 显示逻辑
// WebViewProvider.ts - 添加方法
private currentSessionId: string | null = null;
private currentSessionTitle: string = '';
private async updateCurrentSessionInfo(sessionId: string): Promise<void> {
try {
const sessions = await this.agentManager.getSessionList();
const currentSession = sessions.find(s =>
(s.id === sessionId || s.sessionId === sessionId)
);
if (currentSession) {
const title = this.getSessionTitle(currentSession);
this.currentSessionTitle = title;
this.sendMessageToWebView({
type: 'currentSessionUpdated',
data: { sessionId, title }
});
}
} catch (error) {
console.error('Failed to update session info:', error);
}
}
private getSessionTitle(session: Record<string, unknown>): string {
const title = session.title || session.name;
if (title) return title as string;
// 从第一条消息提取标题
const messages = session.messages as Array<any> || [];
const firstUserMessage = messages.find(m => m.type === 'user');
if (firstUserMessage && firstUserMessage.content) {
return firstUserMessage.content.substring(0, 50) + '...';
}
return 'Untitled Session';
}
// App.tsx - 添加消息处理
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data;
switch (message.type) {
case 'currentSessionUpdated':
setCurrentSessionTitle(message.data.title);
break;
// ... 其他 case
}
};
window.addEventListener('message', messageHandler);
return () => window.removeEventListener('message', messageHandler);
}, []);
任务 5: 添加键盘快捷键支持
文件: package.json
{
"contributes": {
"keybindings": [
{
"command": "qwenCode.openChat",
"key": "ctrl+shift+a",
"mac": "cmd+shift+a"
},
{
"command": "qwenCode.newSession",
"key": "ctrl+shift+n",
"mac": "cmd+shift+n",
"when": "qwenCode.chatVisible"
}
]
}
}
文件: src/extension.ts
context.subscriptions.push(
vscode.commands.registerCommand('qwenCode.newSession', async () => {
await webViewProvider.createNewSession();
}),
);
阶段三: 优化和测试 (1-2 天)
任务 6: Session 切换动画
/* App.css - 添加过渡动画 */
.messages-container {
transition: opacity 0.2s ease-in-out;
}
.messages-container.switching {
opacity: 0.5;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message {
animation: fadeIn 0.3s ease-out;
}
任务 7: 下拉菜单优化
方案 A: 简单下拉(当前模态框改为下拉)
// 将 session-selector-overlay 改为相对定位的下拉菜单
<div className="session-dropdown" ref={dropdownRef}>
{showSessionSelector && (
<div className="session-dropdown-menu">
<div className="session-dropdown-header">
<span>Recent Sessions</span>
<button onClick={handleNewQwenSession}>➕ New</button>
</div>
<div className="session-dropdown-list">
{qwenSessions.map((session) => (
<div
key={session.id}
className="session-dropdown-item"
onClick={() => handleSwitchSession(session.id)}
>
<div className="session-item-title">{getTitle(session)}</div>
<div className="session-item-meta">
{getTimeAgo(session.lastUpdated)}
</div>
</div>
))}
</div>
</div>
)}
</div>
.session-dropdown {
position: relative;
}
.session-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
min-width: 300px;
max-width: 400px;
max-height: 400px;
background-color: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
overflow: hidden;
animation: dropdownSlideIn 0.2s ease-out;
}
@keyframes dropdownSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.session-dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.session-dropdown-list {
max-height: 350px;
overflow-y: auto;
padding: 4px;
}
.session-dropdown-item {
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.session-dropdown-item:hover {
background-color: var(--vscode-list-hoverBackground);
}
.session-dropdown-item.active {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.session-item-title {
font-size: 13px;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-item-meta {
font-size: 11px;
opacity: 0.7;
}
五、风险评估
低风险 ✅
- WebView 位置调整: 只需修改一个参数
- Header 布局重构: 不影响现有功能,纯 UI 调整
- CSS 样式添加: 增量修改,不破坏现有样式
中风险 ⚠️
-
Session 标题提取逻辑: 需要处理多种数据格式
- 缓解措施: 添加完善的 fallback 逻辑
-
下拉菜单点击外部关闭: 需要添加事件监听
- 缓解措施: 使用 React hooks (useEffect + useRef)
无高风险项
六、测试计划
单元测试
- Session 标题提取函数测试
- Session 列表过滤和排序测试
集成测试
- WebView 打开位置验证
- Session 切换流程测试
- 新建 Chat 功能测试
用户体验测试
- 不同窗口布局下的显示效果
- 键盘快捷键功能
- 长 Session 标题的显示
- 主题切换(Light/Dark/High Contrast)
性能测试
- 大量 Session 列表渲染性能
- Session 切换动画流畅度
七、最终建议
✅ 推荐迁移的功能
- WebView 固定右侧: 简单且用户体验提升明显
- Header 重构:
- 左侧 Session 选择器
- 右侧新建按钮
- 下拉菜单样式: 比模态框更符合 IDE 操作习惯
⏸️ 建议延后的功能
- 多种打开方式(Editor/Sidebar/Window): 当前单一方式已足够
- Terminal 模式: Qwen 不需要此功能
- 复杂权限管理: 当前实现已满足需求
📋 实现优先级
P0 (核心功能,必须实现)
- WebView 打开在右侧列
- Header 组件重构(左侧 session,右侧新建)
- 当前 Session 标题显示
P1 (重要优化)
- 下拉菜单替代模态框
- 键盘快捷键支持
- Session 切换动画
P2 (可选增强)
- Session 搜索功能
- Session 固定/收藏
- 最近使用 Session 快速切换
八、时间估算
| 阶段 | 工作量 | 说明 |
|---|---|---|
| 阶段一:基础布局 | 1-2 天 | WebView 位置 + Header 重构 + CSS |
| 阶段二:功能增强 | 2-3 天 | Session 显示 + 快捷键 + 优化 |
| 阶段三:测试调优 | 1-2 天 | 测试 + Bug 修复 + 文档 |
| 总计 | 4-7 天 | 取决于测试覆盖范围 |
九、结论
可行性评估: ✅ 高度可行
-
技术可行性: 100%
- 所需功能均在 VSCode API 支持范围内
- 现有架构完全支持
- 无需引入新的依赖
-
实现复杂度: 低到中等
- 核心改动量小
- 主要是 UI/UX 调整
- 不涉及底层协议变更
-
迁移风险: 低
- 不影响现有核心功能
- 改动均为增量式
- 易于回滚
推荐行动方案
立即可做 (Quick Win)
# 1. 修改 WebView 打开位置
# src/WebViewProvider.ts:77
vscode.ViewColumn.Beside
# 2. 重构 Header 布局
# 预计 2-3 小时即可完成基础版本
短期优化 (1 周内)
- 完整实现 P0 功能
- 添加基础测试
- 文档更新
长期规划 (后续迭代)
- P1/P2 功能根据用户反馈逐步添加
- 性能优化和细节打磨
附录: 参考代码片段
A. 点击外部关闭下拉菜单
const useClickOutside = (
ref: React.RefObject<HTMLElement>,
handler: () => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
};
// 使用
const dropdownRef = useRef<HTMLDivElement>(null);
useClickOutside(dropdownRef, () => setShowSessionSelector(false));
B. Session 时间格式化
function getTimeAgo(timestamp: string | number): string {
const now = Date.now();
const time =
typeof timestamp === 'string' ? new Date(timestamp).getTime() : timestamp;
const diff = now - time;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return new Date(time).toLocaleDateString();
}
C. 渐进式实现策略
// Phase 1: 简单移动
const Header = () => (
<div className="chat-header">
<button onClick={onSessions}>Sessions ▼</button>
<button onClick={onNew}>➕</button>
</div>
);
// Phase 2: 显示当前 Session
const Header = ({ currentSession }) => (
<div className="chat-header">
<button onClick={onSessions}>
{currentSession?.title || 'Select Session'} ▼
</button>
<button onClick={onNew}>➕</button>
</div>
);
// Phase 3: 完整下拉菜单
const Header = ({ currentSession, sessions }) => (
<div className="chat-header">
<Dropdown
current={currentSession}
items={sessions}
onCreate={onNew}
/>
<button onClick={onNew}>➕</button>
</div>
);
文档版本: v1.0 创建日期: 2025-11-18 作者: Claude (Sonnet 4.5) 审核状态: 待审核