mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
402 lines
11 KiB
Markdown
402 lines
11 KiB
Markdown
# WebView Pin 和持久化功能实现完成
|
||
|
||
> **更新时间**: 2025-11-18
|
||
> **状态**: ✅ 实现完成,等待测试
|
||
|
||
---
|
||
|
||
## ✅ 已完成的实现
|
||
|
||
### 1. WebView Pin 功能修复 ✅
|
||
|
||
**问题**: 之前的 pin 功能没有生效
|
||
|
||
**原因**:
|
||
|
||
- `workbench.action.pinEditor` 命令需要在 panel 处于 active 状态时执行
|
||
- 仅使用 setTimeout 不够,需要检查 `panel.active` 状态
|
||
|
||
**解决方案** (`src/WebViewProvider.ts:726-746`):
|
||
|
||
```typescript
|
||
private pinPanel(): void {
|
||
if (!this.panel) {
|
||
return;
|
||
}
|
||
|
||
// 延迟 50ms 并检查 panel 是否为活动状态
|
||
setTimeout(() => {
|
||
if (this.panel && this.panel.active) {
|
||
vscode.commands.executeCommand('workbench.action.pinEditor').then(
|
||
() => {
|
||
console.log('[WebViewProvider] Panel pinned successfully');
|
||
},
|
||
(error) => {
|
||
console.error('[WebViewProvider] Failed to pin panel:', error);
|
||
},
|
||
);
|
||
}
|
||
}, 50);
|
||
}
|
||
```
|
||
|
||
**关键改进**:
|
||
|
||
1. ✅ 检查 `panel.active` 确保 panel 是当前活动编辑器
|
||
2. ✅ 使用 50ms 延迟确保 panel 完全加载
|
||
3. ✅ 添加错误处理和日志记录
|
||
|
||
**触发时机**:
|
||
|
||
- WebView 创建时
|
||
- WebView 重新显示时 (reveal)
|
||
- WebView 视图状态变化时 (onDidChangeViewState)
|
||
|
||
---
|
||
|
||
### 2. WebView 重启后持久化 ✅
|
||
|
||
**问题**: VSCode 重启后,已打开的 WebView tab 会消失
|
||
|
||
**解决方案**: 实现 WebView 序列化机制
|
||
|
||
#### A. 注册 Panel Serializer (`src/extension.ts:123-151`)
|
||
|
||
```typescript
|
||
context.subscriptions.push(
|
||
vscode.window.registerWebviewPanelSerializer('qwenCode.chat', {
|
||
async deserializeWebviewPanel(
|
||
webviewPanel: vscode.WebviewPanel,
|
||
state: unknown,
|
||
) {
|
||
console.log('[Extension] Deserializing WebView panel with state:', state);
|
||
|
||
// 恢复 panel 和事件监听器
|
||
webViewProvider.restorePanel(webviewPanel);
|
||
|
||
// 恢复状态(会话ID、agent初始化状态)
|
||
if (state && typeof state === 'object') {
|
||
webViewProvider.restoreState(
|
||
state as {
|
||
conversationId: string | null;
|
||
agentInitialized: boolean;
|
||
},
|
||
);
|
||
}
|
||
|
||
log('WebView panel restored from serialization');
|
||
},
|
||
}),
|
||
);
|
||
```
|
||
|
||
#### B. 实现 `restorePanel()` 方法 (`src/WebViewProvider.ts:748-799`)
|
||
|
||
```typescript
|
||
restorePanel(panel: vscode.WebviewPanel): void {
|
||
console.log('[WebViewProvider] Restoring WebView panel');
|
||
this.panel = panel;
|
||
|
||
// 设置面板图标
|
||
this.panel.iconPath = vscode.Uri.joinPath(
|
||
this.extensionUri,
|
||
'assets',
|
||
'icon.png',
|
||
);
|
||
|
||
// 设置 webview HTML
|
||
this.panel.webview.html = this.getWebviewContent();
|
||
|
||
// 设置所有事件监听器
|
||
this.panel.webview.onDidReceiveMessage(
|
||
async (message) => {
|
||
await this.handleWebViewMessage(message);
|
||
},
|
||
null,
|
||
this.disposables,
|
||
);
|
||
|
||
this.panel.onDidChangeViewState(
|
||
() => {
|
||
if (this.panel && this.panel.visible) {
|
||
this.pinPanel();
|
||
}
|
||
},
|
||
null,
|
||
this.disposables,
|
||
);
|
||
|
||
this.panel.onDidDispose(
|
||
() => {
|
||
this.panel = null;
|
||
this.disposables.forEach((d) => d.dispose());
|
||
},
|
||
null,
|
||
this.disposables,
|
||
);
|
||
|
||
// 自动 pin 恢复的 panel
|
||
this.pinPanel();
|
||
|
||
console.log('[WebViewProvider] Panel restored successfully');
|
||
}
|
||
```
|
||
|
||
#### C. 实现 `getState()` 方法 (`src/WebViewProvider.ts:801-813`)
|
||
|
||
```typescript
|
||
getState(): {
|
||
conversationId: string | null;
|
||
agentInitialized: boolean;
|
||
} {
|
||
return {
|
||
conversationId: this.currentConversationId,
|
||
agentInitialized: this.agentInitialized,
|
||
};
|
||
}
|
||
```
|
||
|
||
#### D. 实现 `restoreState()` 方法 (`src/WebViewProvider.ts:815-827`)
|
||
|
||
```typescript
|
||
restoreState(state: {
|
||
conversationId: string | null;
|
||
agentInitialized: boolean;
|
||
}): void {
|
||
console.log('[WebViewProvider] Restoring state:', state);
|
||
this.currentConversationId = state.conversationId;
|
||
this.agentInitialized = state.agentInitialized;
|
||
|
||
// 恢复后重新加载内容
|
||
if (this.panel) {
|
||
this.panel.webview.html = this.getWebviewContent();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 实现原理
|
||
|
||
### WebView 序列化流程
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ VSCode 关闭前 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 1. VSCode 检测到有 WebView 打开 │
|
||
│ 2. 调用 webViewProvider.getState() 获取状态 │
|
||
│ 3. 序列化状态到磁盘 │
|
||
│ { │
|
||
│ conversationId: "session-123", │
|
||
│ agentInitialized: true │
|
||
│ } │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ VSCode 重启后 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 1. VSCode 检测到之前有 'qwenCode.chat' WebView │
|
||
│ 2. 查找注册的 serializer (registerWebviewPanelSerializer) │
|
||
│ 3. 创建新的 WebviewPanel 对象 │
|
||
│ 4. 调用 deserializeWebviewPanel() │
|
||
│ ├─ webViewProvider.restorePanel(panel) // 恢复 panel 引用 │
|
||
│ └─ webViewProvider.restoreState(state) // 恢复业务状态 │
|
||
│ 5. WebView 重新出现在编辑器中 │
|
||
│ 6. 自动 pin WebView tab │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 代码改动总结
|
||
|
||
| 文件 | 改动 | 说明 |
|
||
| ------------------------ | ------ | -------------------------------------------------------- |
|
||
| `src/WebViewProvider.ts` | +60 行 | 添加 pinPanel, restorePanel, getState, restoreState 方法 |
|
||
| `src/extension.ts` | +30 行 | 注册 WebView serializer |
|
||
|
||
### 新增方法列表
|
||
|
||
1. `pinPanel()` - Pin WebView tab (line 726-746)
|
||
2. `restorePanel()` - 恢复 panel 和事件监听器 (line 748-799)
|
||
3. `getState()` - 获取序列化状态 (line 801-813)
|
||
4. `restoreState()` - 恢复业务状态 (line 815-827)
|
||
|
||
---
|
||
|
||
## ✅ 验证检查
|
||
|
||
### TypeScript 编译 ✅
|
||
|
||
```bash
|
||
npm run check-types
|
||
# ✅ 通过,无错误
|
||
```
|
||
|
||
### ESLint 检查 ✅
|
||
|
||
```bash
|
||
npm run lint
|
||
# ✅ 通过,无警告
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 测试指南
|
||
|
||
### 测试 1: Pin 功能测试
|
||
|
||
**步骤**:
|
||
|
||
1. 打开 VSCode 调试模式 (F5)
|
||
2. 执行命令 `qwenCode.openChat` 打开 WebView
|
||
3. 观察 WebView tab
|
||
|
||
**预期结果**:
|
||
|
||
- ✅ WebView tab 显示 pin 图标 (📌)
|
||
- ✅ 右键点击其他 tab,选择 "关闭其他编辑器",WebView 不会被关闭
|
||
- ✅ Console 输出: `[WebViewProvider] Panel pinned successfully`
|
||
|
||
---
|
||
|
||
### 测试 2: 重启持久化测试
|
||
|
||
**步骤**:
|
||
|
||
1. 打开 VSCode 调试模式
|
||
2. 执行命令 `qwenCode.openChat` 打开 WebView
|
||
3. 在 WebView 中进行一些操作(如切换 session)
|
||
4. 执行 VSCode 命令 `Developer: Reload Window` 重启窗口
|
||
5. 观察 WebView 是否恢复
|
||
|
||
**预期结果**:
|
||
|
||
- ✅ VSCode 重启后,WebView tab 自动恢复
|
||
- ✅ WebView 仍然在右侧显示
|
||
- ✅ WebView tab 仍然是 pinned 状态
|
||
- ✅ Console 输出:
|
||
```
|
||
[Extension] Deserializing WebView panel with state: {...}
|
||
[WebViewProvider] Restoring WebView panel
|
||
[WebViewProvider] Restoring state: {...}
|
||
[WebViewProvider] Panel restored successfully
|
||
[WebViewProvider] Panel pinned successfully
|
||
```
|
||
|
||
---
|
||
|
||
### 测试 3: 状态恢复测试
|
||
|
||
**步骤**:
|
||
|
||
1. 打开 WebView,切换到某个 session
|
||
2. 记下当前 session ID 和标题
|
||
3. 执行 `Developer: Reload Window`
|
||
4. 检查 WebView 状态
|
||
|
||
**预期结果**:
|
||
|
||
- ✅ 当前 conversation ID 被恢复
|
||
- ✅ agent 初始化状态被恢复
|
||
- ✅ 不需要重新登录或重新连接
|
||
|
||
---
|
||
|
||
### 测试 4: 关闭后重新打开
|
||
|
||
**步骤**:
|
||
|
||
1. 手动关闭 WebView tab (点击 X)
|
||
2. 重新执行 `qwenCode.openChat`
|
||
3. 观察 WebView
|
||
|
||
**预期结果**:
|
||
|
||
- ✅ WebView 在右侧打开
|
||
- ✅ WebView 自动 pinned
|
||
- ✅ 焦点仍在编辑器(不被夺取)
|
||
|
||
---
|
||
|
||
## 🎨 与 Claude Code 对比
|
||
|
||
| 功能 | Claude Code | 当前实现 | 状态 |
|
||
| ------------ | ----------- | -------- | -------- |
|
||
| **Pin Tab** | ✅ | ✅ | 完全对标 |
|
||
| **重启保持** | ✅ | ✅ | 完全对标 |
|
||
| **右侧固定** | ✅ | ✅ | 完全对标 |
|
||
| **不抢焦点** | ✅ | ✅ | 完全对标 |
|
||
| **状态恢复** | ✅ | ✅ | 完全对标 |
|
||
|
||
---
|
||
|
||
## 📝 技术要点
|
||
|
||
### 1. Pin 命令的正确使用
|
||
|
||
```typescript
|
||
// ❌ 错误:直接执行可能不生效
|
||
vscode.commands.executeCommand('workbench.action.pinEditor');
|
||
|
||
// ✅ 正确:检查 active 状态 + 延迟
|
||
setTimeout(() => {
|
||
if (this.panel && this.panel.active) {
|
||
vscode.commands.executeCommand('workbench.action.pinEditor');
|
||
}
|
||
}, 50);
|
||
```
|
||
|
||
### 2. Serializer 注册时机
|
||
|
||
必须在 extension.ts 的 `activate()` 函数中注册,且必须在 `context.subscriptions` 中添加:
|
||
|
||
```typescript
|
||
context.subscriptions.push(
|
||
vscode.window.registerWebviewPanelSerializer('qwenCode.chat', {
|
||
async deserializeWebviewPanel(...) { ... }
|
||
})
|
||
);
|
||
```
|
||
|
||
### 3. 事件监听器清理
|
||
|
||
在 `restorePanel()` 中设置的所有监听器都添加到 `this.disposables`,确保在 dispose 时正确清理:
|
||
|
||
```typescript
|
||
this.panel.webview.onDidReceiveMessage(
|
||
async (message) => { ... },
|
||
null,
|
||
this.disposables, // ← 重要!
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 下一步
|
||
|
||
### 立即测试
|
||
|
||
1. 启动 VSCode 调试模式 (F5)
|
||
2. 按照上面的测试指南逐项测试
|
||
3. 记录测试结果
|
||
|
||
### 如果测试通过
|
||
|
||
- 提交代码到 git
|
||
- 合并到主分支
|
||
- 更新版本号
|
||
|
||
### 如果发现问题
|
||
|
||
- 在 Console 中查看错误日志
|
||
- 检查 `[WebViewProvider]` 和 `[Extension]` 的日志输出
|
||
- 记录问题并修复
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**创建时间**: 2025-11-18
|
||
**状态**: ✅ 实现完成,等待测试
|