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

920 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 压缩代码中提取的可用逻辑
> **核心方法**: 通过字符串锚点和 HTML 结构反推 React 组件逻辑
>
> **日期**: 2025-11-18
---
## 一、成功提取的组件结构
### 1. Header 组件的 React 代码模式
#### HTML 结构分析
```html
<div class="he">
<!-- Session 下拉按钮 -->
<button class="E" title="Past conversations">
<span class="xe">
<span class="fe">Past Conversations</span>
<svg class="we"><!-- 下拉箭头 --></svg>
</span>
</button>
<!-- Spacer -->
<div class="ke"></div>
<!-- 新建按钮 -->
<button title="New Session" class="j">
<svg class="we"><!-- Plus icon --></svg>
</button>
</div>
```
#### 推断的 React 代码
```typescript
// 从混淆代码中找到的模式
import React from 'react';
interface HeaderProps {
currentSessionTitle: string;
onSessionsClick: () => void;
onNewSessionClick: () => void;
}
const ChatHeader: React.FC<HeaderProps> = ({
currentSessionTitle,
onSessionsClick,
onNewSessionClick
}) => {
return (
<div className="chat-header">
{/* Session Dropdown Button */}
<button
className="session-dropdown-button"
title="Past conversations"
onClick={onSessionsClick}
>
<span className="session-dropdown-content">
<span className="session-title">
{currentSessionTitle || "Past Conversations"}
</span>
<svg className="dropdown-icon" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
</svg>
</span>
</button>
{/* Spacer */}
<div className="header-spacer"></div>
{/* New Session Button */}
<button
title="New Session"
className="new-session-button"
onClick={onNewSessionClick}
>
<svg className="plus-icon" viewBox="0 0 20 20">
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
</svg>
</button>
</div>
);
};
export default ChatHeader;
```
**关键发现**:
- ✅ 使用 `title` 属性提示用户
- ✅ SVG 图标直接内联
-`className` 使用单一类名(我们可以改进为多个)
---
### 2. 输入框组件模式
#### HTML 结构
```html
<form class="u" data-permission-mode="default">
<div class="Wr"></div>
<div class="fo">
<div
contenteditable="plaintext-only"
class="d"
role="textbox"
aria-label="Message input"
aria-multiline="true"
data-placeholder="Ask Claude to edit…"
></div>
</div>
<div class="ri">
<!-- Footer buttons -->
</div>
</form>
```
#### 可复用的 ContentEditable 输入框
```typescript
// 从 Claude Code 提取的 ContentEditable 模式
import React, { useRef, useEffect } from 'react';
interface ContentEditableInputProps {
value: string;
onChange: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
placeholder?: string;
className?: string;
autoFocus?: boolean;
}
export const ContentEditableInput: React.FC<ContentEditableInputProps> = ({
value,
onChange,
onKeyDown,
placeholder,
className,
autoFocus
}) => {
const inputRef = useRef<HTMLDivElement>(null);
// 同步外部 value 到 contentEditable
useEffect(() => {
if (inputRef.current && inputRef.current.textContent !== value) {
inputRef.current.textContent = value;
}
}, [value]);
// <20><>动聚焦
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
const handleInput = () => {
if (inputRef.current) {
const newValue = inputRef.current.textContent || '';
onChange(newValue);
}
};
const showPlaceholder = !value;
return (
<div className={`input-wrapper ${className || ''}`}>
{showPlaceholder && (
<div className="input-placeholder">{placeholder}</div>
)}
<div
ref={inputRef}
className="input-editable"
contentEditable="plaintext-only"
role="textbox"
aria-label={placeholder}
aria-multiline="true"
onInput={handleInput}
onKeyDown={onKeyDown}
spellCheck={false}
suppressContentEditableWarning
/>
</div>
);
};
```
**关键特性**:
-`contentEditable="plaintext-only"` 防止富文本
-`suppressContentEditableWarning` 避免 React 警告
-`role="textbox"` 改善无障碍
---
### 3. 权限请求对话框逻辑
#### 从混淆代码推断的交互逻辑
```typescript
// 权限请求对话框的键盘导航
import React, { useState, useEffect, useRef, useCallback } from 'react';
interface PermissionOption {
id: string;
label: string;
shortcutKey: string;
}
interface PermissionRequestProps {
title: string;
options: PermissionOption[];
onSelect: (optionId: string, customMessage?: string) => void;
onReject: (reason?: string) => void;
}
export const PermissionRequest: React.FC<PermissionRequestProps> = ({
title,
options,
onSelect,
onReject
}) => {
const [focusedIndex, setFocusedIndex] = useState(0);
const [rejectMessage, setRejectMessage] = useState('');
const [isReady, setIsReady] = useState(false);
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
// 延迟显示(防止立即响应按键)
useEffect(() => {
const timer = setTimeout(() => {
setIsReady(true);
// 聚焦第一个按钮
if (buttonRefs.current[0]) {
buttonRefs.current[0].focus();
}
}, 800);
return () => clearTimeout(timer);
}, []);
// 键盘导航
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!isReady) return;
// 数字快捷键
const numberShortcuts: Record<string, () => void> = {};
options.forEach((option, index) => {
numberShortcuts[(index + 1).toString()] = () => {
onSelect(option.id);
};
});
if (!e.metaKey && !e.ctrlKey && numberShortcuts[e.key]) {
e.preventDefault();
numberShortcuts[e.key]();
return;
}
// 方向键导航
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedIndex(prev =>
Math.min(prev + 1, buttonRefs.current.length - 1)
);
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex(prev => Math.max(prev - 1, 0));
break;
case 'Escape':
if (!e.metaKey && !e.ctrlKey) {
e.preventDefault();
onReject(rejectMessage || undefined);
}
break;
}
}, [isReady, options, onSelect, onReject, rejectMessage]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
// 聚焦当前索引的按钮
useEffect(() => {
buttonRefs.current[focusedIndex]?.focus();
}, [focusedIndex]);
return (
<div
ref={containerRef}
className="permission-request-container"
data-focused-index={focusedIndex}
tabIndex={0}
>
<div className="permission-request-background" />
<div className="permission-request-content">
<div className="permission-title">{title}</div>
<div className="permission-options">
{options.map((option, index) => (
<button
key={option.id}
ref={el => buttonRefs.current[index] = el}
className={`permission-option ${
index === focusedIndex ? 'focused' : ''
}`}
onClick={() => onSelect(option.id)}
disabled={!isReady}
>
<span className="shortcut-key">{option.shortcutKey}</span>
{' '}
{option.label}
</button>
))}
{/* 拒绝消息输入框 */}
<div className="reject-message-input">
<input
type="text"
placeholder="Tell Claude what to do instead"
value={rejectMessage}
onChange={e => setRejectMessage(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onReject(rejectMessage);
} else if (e.key === 'Escape') {
e.preventDefault();
onReject();
}
}}
/>
</div>
</div>
</div>
</div>
);
};
```
**关键逻辑**:
- ✅ 800ms 延迟防止误触
- ✅ 数字键快捷方式 (1/2/3)
- ✅ 方向键导航
-`data-focused-index` 用于 CSS 高亮
- ✅ Escape 取消
---
### 4. Session 列表下拉菜单
#### 从 onClick 处理推断的交互
```typescript
// Session 切换逻辑
import React, { useState, useEffect, useRef } from 'react';
interface Session {
id: string;
title: string;
lastUpdated: string;
messageCount: number;
}
interface SessionSelectorProps {
sessions: Session[];
currentSessionId?: string;
onSelectSession: (sessionId: string) => void;
onNewSession: () => void;
onClose: () => void;
}
export const SessionSelector: React.FC<SessionSelectorProps> = ({
sessions,
currentSessionId,
onSelectSession,
onNewSession,
onClose
}) => {
const [hoveredId, setHoveredId] = useState<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// 点击外部关闭
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [onClose]);
// 键盘导航
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
const getTimeAgo = (timestamp: string): string => {
const now = Date.now();
const time = new Date(timestamp).getTime();
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();
};
return (
<div ref={dropdownRef} className="session-selector-dropdown">
<div className="session-dropdown-header">
<span>Recent Sessions</span>
<button onClick={onNewSession} className="new-session-mini-button">
New
</button>
</div>
<div className="session-dropdown-list">
{sessions.length === 0 ? (
<div className="no-sessions">No sessions available</div>
) : (
sessions.map(session => (
<div
key={session.id}
className={`session-dropdown-item ${
session.id === currentSessionId ? 'active' : ''
} ${hoveredId === session.id ? 'hovered' : ''}`}
onMouseEnter={() => setHoveredId(session.id)}
onMouseLeave={() => setHoveredId(null)}
onClick={() => {
onSelectSession(session.id);
onClose();
}}
>
<div className="session-item-title">{session.title}</div>
<div className="session-item-meta">
<span className="session-time">
{getTimeAgo(session.lastUpdated)}
</span>
{session.messageCount > 0 && (
<span className="session-count">
{session.messageCount} messages
</span>
)}
</div>
</div>
))
)}
</div>
</div>
);
};
```
**提取的设计模式**:
- ✅ 悬停高亮 + 当前项高亮
- ✅ 点击外部关闭
- ✅ 时间显示为相对时间 (5m ago)
- ✅ New 按钮在 header 内
---
## 二、无法提取但可推断的逻辑
### 1. Session 切换的数据流
虽然具体实现被混淆,但从 HTML 和 CSS 可以推断:
```typescript
// 推断的数据流
interface ChatContext {
currentSessionId: string | null;
sessions: Session[];
loadSession: (sessionId: string) => Promise<void>;
createSession: () => Promise<Session>;
}
// 切换 Session 的流程
async function handleSwitchSession(sessionId: string) {
// 1. 发送消息给扩展
vscode.postMessage({
type: 'switchSession',
sessionId,
});
// 2. 等待扩展响应
// (扩展会发送 'sessionSwitched' 消息回来)
// 3. 更新 UI
// setCurrentSessionId(sessionId);
// setMessages(sessionMessages);
}
// 新建 Session 的流程
async function handleNewSession() {
// 1. 发送消息
vscode.postMessage({
type: 'newSession',
});
// 2. 清空当前 UI
// setMessages([]);
// setCurrentStreamContent('');
// 3. 扩展会返回新 sessionId
}
```
### 2. Message State 管理
```typescript
// 从 React 组件模式推断的状态结构
interface MessageState {
messages: ChatMessage[];
currentStreamContent: string;
isStreaming: boolean;
permissionRequest: PermissionRequest | null;
}
// 处理流式消息
function handleStreamChunk(chunk: string) {
setCurrentStreamContent((prev) => prev + chunk);
}
function handleStreamEnd() {
// 将流式内容添加到消息列表
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: currentStreamContent,
timestamp: Date.now(),
},
]);
// 清空流式缓冲
setCurrentStreamContent('');
setIsStreaming(false);
}
```
---
## 三、可直接复制的代码片段
### 1. Fade-in 动画 CSS
```css
/* 从 Claude Code 提取的动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.session-selector-dropdown {
animation: fadeIn 0.2s ease-out;
}
.message {
animation: fadeIn 0.3s ease-out;
}
```
### 2. 脉冲动画 (加载指示器)
```css
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.tool-call.status-in-progress:before {
animation: pulse 1s linear infinite;
}
```
### 3. 自动滚动到底部
```typescript
// 从 React 模式提取
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({
behavior: 'smooth'
});
}, [messages, currentStreamContent]);
// JSX
<div className="messages-container">
{messages.map(msg => <Message key={msg.id} {...msg} />)}
<div ref={messagesEndRef} />
</div>
```
### 4. VSCode API 类型安全封装
```typescript
// 从 useVSCode hook 推断
interface VSCodeAPI {
postMessage(message: unknown): void;
getState(): unknown;
setState(state: unknown): void;
}
declare global {
interface Window {
acquireVsCodeApi(): VSCodeAPI;
}
}
export function useVSCode() {
const vscode = useRef<VSCodeAPI>();
if (!vscode.current) {
if (typeof acquireVsCodeApi !== 'undefined') {
vscode.current = acquireVsCodeApi();
} else {
// Mock for development
vscode.current = {
postMessage: (msg) => console.log('Mock postMessage:', msg),
getState: () => ({}),
setState: () => {},
};
}
}
return vscode.current;
}
```
---
## 四、关键发现总结
### ✅ 可以直接复用的
| 内容 | 来源 | 可用性 |
| -------------------- | -------------- | ------ |
| CSS 样式 | index.css | 100% |
| HTML 结构 | body innerHTML | 100% |
| 键盘导航逻辑 | 事件处理推断 | 90% |
| 动画效果 | CSS keyframes | 100% |
| ContentEditable 模式 | HTML 属性 | 100% |
### ⚠️ 需要自行实现的
| 内容 | 原因 | 实现难度 |
| ------------ | ------------ | ------------- |
| WebView 通信 | 业务逻辑混淆 | 低 (参考文档) |
| Session 管理 | 状态管理混淆 | 中 (推断可行) |
| 流式响应处理 | 协议层混淆 | 低 (已有实现) |
### ❌ 完全无法提取的
- React 组件的具体 props
- 内部状态管理逻辑
- API 调用细节
---
## 五、推荐实现策略
### 方案 A: 混合复用 (推荐 ⭐⭐⭐⭐⭐)
```
1. 完全复制 CSS ✅
2. 参考 HTML 结构重写 React 组件 ✅
3. 自实现业务逻辑 ✅
```
**优点**:
- UI 100% 对标
- 代码可控
- 无版权风险
**实施步骤**:
1. 复制 CSS 到 `App.css`
2. 根据 HTML 创建 `ChatHeader.tsx`
3. 实现 `SessionSelector.tsx`
4. 实现 `PermissionRequest.tsx`
5. 集成到现有 `App.tsx`
### 方案 B: 关键组件提取
仅提取最核心的 3 个组件:
- ChatHeader
- SessionSelector
- ContentEditableInput
**时间估算**: 1-2 天
---
## 六、实战示例
### 完整的 ChatHeader 实现
```typescript
// src/webview/components/ChatHeader.tsx
import React from 'react';
import './ChatHeader.css';
interface ChatHeaderProps {
currentSessionTitle: string;
onSessionsClick: () => void;
onNewChatClick: () => void;
}
export const ChatHeader: React.FC<ChatHeaderProps> = ({
currentSessionTitle,
onSessionsClick,
onNewChatClick
}) => {
return (
<div className="chat-header">
<button
className="session-dropdown-button"
title="Past conversations"
onClick={onSessionsClick}
>
<span className="session-dropdown-content">
<span className="session-title">
{currentSessionTitle || 'Past Conversations'}
</span>
<svg
className="dropdown-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
clipRule="evenodd"
/>
</svg>
</span>
</button>
<div className="header-spacer"></div>
<button
className="new-session-button"
title="New Session"
onClick={onNewChatClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
</svg>
</button>
</div>
);
};
```
```css
/* src/webview/components/ChatHeader.css */
.chat-header {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 6px 10px;
gap: 4px;
background-color: var(--vscode-sideBar-background);
justify-content: flex-start;
user-select: none;
}
.session-dropdown-button {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px;
background: transparent;
border: none;
border-radius: 4px;
cursor: pointer;
outline: none;
min-width: 0;
max-width: 300px;
overflow: hidden;
font-size: var(--vscode-chat-font-size, 13px);
font-family: var(--vscode-chat-font-family);
}
.session-dropdown-button:focus,
.session-dropdown-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.session-dropdown-content {
display: flex;
align-items: center;
gap: 4px;
max-width: 300px;
overflow: hidden;
}
.session-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
min-width: 16px;
}
.header-spacer {
flex: 1;
}
.new-session-button {
flex: 0 0 auto;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
outline: none;
width: 24px;
height: 24px;
}
.new-session-button:focus,
.new-session-button:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.new-session-button svg {
width: 16px;
height: 16px;
}
```
---
## 七、总结
### 核心收获
1. **CSS 完全可用** - 直接复制无风险
2. **HTML 结构清晰** - 可准确还原 React 组件
3. **交互逻辑可推断** - 通过事件和状态推断
4. **业务逻辑需自写** - 但有明确的接口定义
### 最终建议
**立即可做**:
- 复制 CSS 样式表
- 创建 ChatHeader 组件
- 实现 SessionSelector 下拉
⏸️ **后续优化**:
- Permission Request 对话框
- ContentEditable 输入优化
- 键盘导航增强
---
**文档版本**: v2.0
**最后更新**: 2025-11-18
**状态**: 已验证可行