wip(vscode-ide-companion): 实现 quick win 功能

- 将 WebView 调整到编辑器右侧
- 添加 ChatHeader 组件,实现会话下拉菜单
- 替换模态框为紧凑型下拉菜单
- 更新会话切换逻辑,显示当前标题
- 清理旧的会话选择器样式
基于 Claude Code v2.0.43 UI 分析实现。
This commit is contained in:
yiliang114
2025-11-19 00:16:45 +08:00
parent 729a3d0ab3
commit 732220e651
52 changed files with 16502 additions and 1420 deletions

View File

@@ -0,0 +1,401 @@
# 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
**状态**: ✅ 实现完成,等待测试