Files
qwen-code/packages/vscode-ide-companion/docs-tmp/WEBVIEW_PERSISTENCE_IMPLEMENTATION.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

11 KiB
Raw Blame History

WebView Pin 和持久化功能实现完成

更新时间: 2025-11-18 状态: 实现完成,等待测试


已完成的实现

1. WebView Pin 功能修复

问题: 之前的 pin 功能没有生效

原因:

  • workbench.action.pinEditor 命令需要在 panel 处于 active 状态时执行
  • 仅使用 setTimeout 不够,需要检查 panel.active 状态

解决方案 (src/WebViewProvider.ts:726-746):

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)

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)

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)

getState(): {
  conversationId: string | null;
  agentInitialized: boolean;
} {
  return {
    conversationId: this.currentConversationId,
    agentInitialized: this.agentInitialized,
  };
}

D. 实现 restoreState() 方法 (src/WebViewProvider.ts:815-827)

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 编译

npm run check-types
# ✅ 通过,无错误

ESLint 检查

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 命令的正确使用

// ❌ 错误:直接执行可能不生效
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 中添加:

context.subscriptions.push(
  vscode.window.registerWebviewPanelSerializer('qwenCode.chat', {
    async deserializeWebviewPanel(...) { ... }
  })
);

3. 事件监听器清理

restorePanel() 中设置的所有监听器都添加到 this.disposables,确保在 dispose 时正确清理:

this.panel.webview.onDidReceiveMessage(
  async (message) => { ... },
  null,
  this.disposables, // ← 重要!
);

🚀 下一步

立即测试

  1. 启动 VSCode 调试模式 (F5)
  2. 按照上面的测试指南逐项测试
  3. 记录测试结果

如果测试通过

  • 提交代码到 git
  • 合并到主分支
  • 更新版本号

如果发现问题

  • 在 Console 中查看错误日志
  • 检查 [WebViewProvider][Extension] 的日志输出
  • 记录问题并修复

文档版本: v1.0 创建时间: 2025-11-18 状态: 实现完成,等待测试