Files
qwen-code/packages/vscode-ide-companion/docs-tmp/MIGRATION_FEASIBILITY.md
yiliang114 732220e651 wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧
- 添加 ChatHeader 组件,实现会话下拉菜单
- 替换模态框为紧凑型下拉菜单
- 更新会话切换逻辑,显示当前标题
- 清理旧的会话选择器样式
基于 Claude Code v2.0.43 UI 分析实现。
2025-11-19 00:16:45 +08:00

982 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 扩展: 支持多种布局方式
**可行性**: ✅ **完全可行**
**实现方案**:
```typescript
// 当前实现 (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: 移动现有按钮到左侧**
```tsx
// 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: 使用真正的下拉选择**
```tsx
// 使用 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 样式**:
```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` 类(图标按钮样式)
**可行性**: ✅ **完全可行**
**实现方案**:
```tsx
// 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 样式**:
```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. 多种打开方式
```json
{
"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
```json
{
"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`
```typescript
// 修改 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`
```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`
```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 显示逻辑
```typescript
// 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';
}
```
```tsx
// 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`
```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`
```typescript
context.subscriptions.push(
vscode.commands.registerCommand('qwenCode.newSession', async () => {
await webViewProvider.createNewSession();
}),
);
```
### 阶段三: 优化和测试 (1-2 天)
#### 任务 6: Session 切换动画
```css
/* 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: 简单下拉(当前模态框改为下拉)**
```tsx
// 将 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>
```
```css
.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;
}
```
---
## 五、风险评估
### 低风险 ✅
1. **WebView 位置调整**: 只需修改一个参数
2. **Header 布局重构**: 不影响现有功能,纯 UI 调整
3. **CSS 样式添加**: 增量修改,不破坏现有样式
### 中风险 ⚠️
1. **Session 标题提取逻辑**: 需要处理多种数据格式
- **缓解措施**: 添加完善的 fallback 逻辑
2. **下拉菜单点击外部关闭**: 需要添加事件监听
- **缓解措施**: 使用 React hooks (useEffect + useRef)
### 无高风险项
---
## 六、测试计划
### 单元测试
- [ ] Session 标题提取函数测试
- [ ] Session 列表过滤和排序测试
### 集成测试
- [ ] WebView 打开位置验证
- [ ] Session 切换流程测试
- [ ] 新建 Chat 功能测试
### 用户体验测试
- [ ] 不同窗口布局下的显示效果
- [ ] 键盘快捷键功能
- [ ] 长 Session 标题的显示
- [ ] 主题切换(Light/Dark/High Contrast)
### 性能测试
- [ ] 大量 Session 列表渲染性能
- [ ] Session 切换动画流畅度
---
## 七、最终建议
### ✅ 推荐迁移的功能
1. **WebView 固定右侧**: 简单且用户体验提升明显
2. **Header 重构**:
- 左侧 Session 选择器
- 右侧新建按钮
3. **下拉菜单样式**: 比模态框更符合 IDE 操作习惯
### ⏸️ 建议延后的功能
1. **多种打开方式**(Editor/Sidebar/Window): 当前单一方式已足够
2. **Terminal 模式**: Qwen 不需要此功能
3. **复杂权限管理**: 当前实现已满足需求
### 📋 实现优先级
#### P0 (核心功能,必须实现)
1. WebView 打开在右侧列
2. Header 组件重构(左侧 session,右侧新建)
3. 当前 Session 标题显示
#### P1 (重要优化)
1. 下拉菜单替代模态框
2. 键盘快捷键支持
3. Session 切换动画
#### P2 (可选增强)
1. Session 搜索功能
2. Session 固定/收藏
3. 最近使用 Session 快速切换
---
## 八、时间估算
| 阶段 | 工作量 | 说明 |
| --------------- | ---------- | -------------------------------- |
| 阶段一:基础布局 | 1-2 天 | WebView 位置 + Header 重构 + CSS |
| 阶段二:功能增强 | 2-3 天 | Session 显示 + 快捷键 + 优化 |
| 阶段三:测试调优 | 1-2 天 | 测试 + Bug 修复 + 文档 |
| **总计** | **4-7 天** | 取决于测试覆盖范围 |
---
## 九、结论
### 可行性评估: ✅ **高度可行**
1. **技术可行性**: 100%
- 所需功能均在 VSCode API 支持范围内
- 现有架构完全支持
- 无需引入新的依赖
2. **实现复杂度**: 低到中等
- 核心改动量小
- 主要是 UI/UX 调整
- 不涉及底层协议变更
3. **迁移风险**: 低
- 不影响现有核心功能
- 改动均为增量式
- 易于回滚
### 推荐行动方案
#### 立即可做 (Quick Win)
```bash
# 1. 修改 WebView 打开位置
# src/WebViewProvider.ts:77
vscode.ViewColumn.Beside
# 2. 重构 Header 布局
# 预计 2-3 小时即可完成基础版本
```
#### 短期优化 (1 周内)
- 完整实现 P0 功能
- 添加基础测试
- 文档更新
#### 长期规划 (后续迭代)
- P1/P2 功能根据用户反馈逐步添加
- 性能优化和细节打磨
---
## 附录: 参考代码片段
### A. 点击外部关闭下拉菜单
```tsx
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 时间格式化
```typescript
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. 渐进式实现策略
```typescript
// 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)
**审核状态**: 待审核