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

22 KiB

Claude Code HTML 结构到混淆 JS 的完整映射

方法论: 通过 HTML 类名 + 字符串锚点定位混淆代码中的组件逻辑

日期: 2025-11-18


一、HTML 结构完整分析

1. 顶层结构

<body class="vscode-dark">
  <div id="root">
    <div class="me">               <!-- 主容器 -->
      <div class="he">             <!-- Header 区域 -->
      <div class="be">             <!-- 主内容区 -->
        <div class="Q">
          <div class="ue">
            <div class="ye">       <!-- 消息容器 -->
              <div class="Re">     <!-- 空状态 -->
            </div>
          </div>
        </div>
        <div>                      <!-- 输入区域 -->
          <form class="u">         <!-- 输入表单 -->
            <div class="fo">       <!-- 消息输入容器 -->
            <div class="ri">       <!-- Footer 按钮 -->
          </form>
        </div>
      </div>
    </div>
  </div>
</body>

二、关键组件的 CSS 映射表

HTML 类名 用途 JS 变量名 (推断) 位置
.me 主容器 - index.css
.he Header - index.css
.E Session 按钮 c_ / tm xKe 函数
.j New Session 按钮 zl Footer 定义
.u 输入表单 c_ ContentEditable
.d 输入框 - ContentEditable
.ri Footer 按钮区 zl Footer 组件
.l Footer 按钮 zl Footer 按钮

三、成功映射的组件逻辑

A. 命令菜单组件 (Command Menu)

HTML 定位线索

  • 类名: .menuPopup, .commandList, .commandItem
  • 字符串: "Filter actions...", "No matching commands"

找到的 JS 代码

// 从混淆代码提取 - 命令菜单组件
var xKe = ({
  isOpen: n,
  onClose: e,
  onCommandSelect: t,
  commandRegistry: i,
  filterText: o,
  suppressFilter: r = !1,
}) => {
  let [s, a] = (0, rr.useState)(''); // filterText state
  let [l, c] = (0, rr.useState)(null); // selectedId state
  let d = (0, rr.useRef)(null); // input ref
  let u = (0, rr.useRef)(null); // container ref
  let h = (0, rr.useRef)(null); // selected item ref

  let f = r ? o || '' : s || ''; // 实际过滤文本

  // 获取命令分组
  let p = i.getCommandsBySection();

  // 过滤命令
  let g = Object.entries(p).reduce((x, [w, y]) => {
    let C = y.filter((L) => L.label.toLowerCase().includes(f.toLowerCase()));
    return (C.length > 0 && (x[w] = C), x);
  }, {});

  let _ = Object.values(g).flat(); // 扁平化命令列表

  // 键盘导航逻辑
  let b = (0, rr.useCallback)(
    (x) => {
      if (x.key === 'Escape') {
        x.preventDefault();
        e();
        return;
      }

      if (x.key === 'ArrowDown' && _.length > 0) {
        x.preventDefault();
        let w = _.findIndex((C) => C.id === l);
        let y = w < _.length - 1 ? w + 1 : 0;
        c(_[y]?.id || null);
        return;
      }

      if (x.key === 'ArrowUp' && _.length > 0) {
        x.preventDefault();
        let w = _.findIndex((C) => C.id === l);
        let y = w > 0 ? w - 1 : _.length - 1;
        c(_[y]?.id || null);
        return;
      }

      if ((x.key === 'Tab' || x.key === 'Enter') && !x.shiftKey) {
        if ('isComposing' in x && x.isComposing) return;
        x.preventDefault();
        if (l) {
          let w = _.find((y) => y.id === l);
          if (w) {
            a('');
            let y = x.key === 'Tab';
            t(w, y);
          }
        }
        return;
      }
    },
    [l, _, e, t],
  )(
    // 滚动到选中项
    0,
    rr.useEffect,
  )(() => {
    h.current &&
      h.current.scrollIntoView({
        behavior: 'instant',
        block: 'nearest',
      });
  }, [l]);

  // 返回 JSX
  return n
    ? rr.default.createElement(
        'div',
        {
          ref: u,
          className: tm.menuPopup,
        },
        // 过滤输入框
        !r &&
          rr.default.createElement('input', {
            ref: d,
            type: 'text',
            value: f,
            onChange: (x) => a(x.target.value),
            onKeyDown: v,
            placeholder: 'Filter actions...',
            className: tm.filterInput,
          }),

        // 命令列表
        rr.default.createElement(
          'div',
          {
            className: tm.commandList,
          },
          Object.keys(g).length === 0
            ? rr.default.createElement(
                'div',
                {
                  className: tm.emptyState,
                },
                'No matching commands',
              )
            : Object.entries(g).map(([x, w], y) =>
                // 每个分组
                rr.default.createElement(
                  'div',
                  { key: x },
                  // 分组标题
                  rr.default.createElement(
                    'div',
                    {
                      className: tm.sectionHeader,
                    },
                    x,
                  ),

                  // 命令项
                  w.map((C) =>
                    rr.default.createElement(
                      'div',
                      {
                        key: C.id,
                        ref: C.id === l ? h : null,
                        className: `${tm.commandItem} ${C.id === l ? tm.activeCommandItem : ''}`,
                        onMouseEnter: () => c(C.id),
                        onClick: () => {
                          a('');
                          t(C, !1);
                        },
                      },
                      rr.default.createElement(
                        'div',
                        {
                          className: tm.commandContent,
                        },
                        rr.default.createElement(
                          'span',
                          {
                            className: tm.commandLabel,
                          },
                          C.label,
                        ),
                        C.trailingComponent,
                      ),
                    ),
                  ),
                ),
              ),
        ),
      )
    : null;
};

关键发现:

  • 使用 useState 管理过滤文本和选中项
  • useRef 管理输入框和列表项的引用
  • 键盘导航: Escape/ArrowUp/ArrowDown/Tab/Enter
  • scrollIntoView 自动滚动到选中项

B. 文件选择器组件 (File Selector)

HTML 定位线索

  • 类名: .fileList, .fileItem, .fileName
  • 字符串: "No files found"

找到的 JS 代码

// 文件选择器组件
var wKe = ({ searchQuery: n, onClose: e, onFileSelect: t, onListFiles: i }) => {
  let [o, r] = (0, ao.useState)([]); // files state
  let [s, a] = (0, ao.useState)(0); // selectedIndex state
  let l = (0, ao.useRef)(null);
  let c = (0, ao.useRef)(null)(
    // 防抖加载文件列表
    0,
    ao.useEffect,
  )(() => {
    let u = setTimeout(() => {
      i(n)
        .then((h) => {
          r(h);
          a(0);
        })
        .catch((h) => {});
    }, 200); // 200ms 防抖
    return () => clearTimeout(u);
  }, [i, n]);

  // 键盘导航
  let d = (0, ao.useCallback)(
    (u) => {
      switch (u.key) {
        case 'ArrowDown':
          o.length > 1 &&
            (u.preventDefault(), a((h) => (h < o.length - 1 ? h + 1 : 0)));
          break;
        case 'ArrowUp':
          o.length > 1 &&
            (u.preventDefault(), a((h) => (h > 0 ? h - 1 : o.length - 1)));
          break;
        case 'Tab':
        case 'Enter':
          u.shiftKey || (u.preventDefault(), o[s] && t(o[s], u.key === 'Tab'));
          break;
        case 'Escape':
          u.preventDefault();
          e();
          break;
      }
    },
    [o, s, t, e],
  )(
    // 滚动到选中项
    0,
    ao.useEffect,
  )(() => {
    c.current &&
      c.current.scrollIntoView({
        behavior: 'instant',
        block: 'nearest',
      });
  }, [s])(
    // 监听全局键盘事件
    0,
    ao.useEffect,
  )(
    () => (
      document.addEventListener('keydown', d),
      () => document.removeEventListener('keydown', d)
    ),
    [d],
  );

  // 渲染文件列表
  return ao.default.createElement(
    'div',
    {
      ref: l,
      className: $u.menuPopup,
    },
    ao.default.createElement(
      'div',
      {
        className: $u.fileList,
      },
      o.length === 0
        ? ao.default.createElement(
            'div',
            {
              className: $u.emptyState,
            },
            'No files found',
          )
        : o.map((u, h) =>
            ao.default.createElement(
              'div',
              {
                key: u.path,
                ref: h === s ? c : null,
                className: `${$u.fileItem} ${h === s ? $u.activeFileItem : ''}`,
                onMouseEnter: () => a(h),
                onClick: () => t(u, !1),
              },
              ao.default.createElement(
                'div',
                {
                  className: $u.fileContent,
                },
                // 文件图标和名称
                u.type === 'file'
                  ? ao.default.createElement(
                      'div',
                      {
                        className: $u.fileName,
                      },
                      u.name,
                    )
                  : ao.default.createElement(
                      'div',
                      {
                        className: $u.directoryPath,
                      },
                      u.path,
                    ),
              ),
            ),
          ),
    ),
  );
};

关键发现:

  • 200ms 防抖加载
  • 全局键盘事件监听
  • Tab 键选中文件后继续输入

HTML 定位线索

  • 类名: .inputFooter, .footerButton, .sendButton
  • 字符串: "Ask before edits", "Thinking off"

找到的 JS 代码

// Footer 组件
function CKe({
  session: n,
  mode: e,
  onCycleMode: t,
  currentSelection: i,
  canSendMessage: o,
  toggleCommandMenu: r,
  includeSelection: s,
  onToggleIncludeSelection: a,
  onCompact: l,
  onAttachFile: c,
}) {
  // 发送图标切换
  let d = null;
  n.busy.value && !o
    ? (d = lo.default.createElement(rie, { className: zl.stopIcon }))
    : (d = lo.default.createElement(Xte, { className: zl.sendIcon }));

  // Thinking 开关
  let u = n.thinkingLevel.value !== 'off';
  let h = () => {
    n.setThinkingLevel(n.thinkingLevel.value === 'off' ? 'default_on' : 'off');
  };

  return lo.default.createElement(
    'div',
    {
      className: zl.inputFooter,
    },
    // Mode 切换按钮
    lo.default.createElement(DSt, {
      mode: e,
      onTap: t,
    }),

    // 文件选择按钮
    i &&
      lo.default.createElement(NSt, {
        includeSelection: s ?? !1,
        currentSelection: i,
        onToggle: a ?? (() => {}),
      }),

    // Usage 指示器
    lo.default.createElement(yKe, {
      usedTokens: n.usageData.value.totalTokens,
      contextWindow: n.usageData.value.contextWindow,
      onCompact: l,
    }),

    // Spacer
    lo.default.createElement('div', {
      className: zl.spacer,
    }),

    // Thinking 按钮
    lo.default.createElement(RSt, {
      thinkingOn: u,
      toggleThinking: h,
    }),

    // 命令菜单按钮
    lo.default.createElement(
      'button',
      {
        type: 'button',
        className: zl.menuButton,
        title: 'Show command menu (/)',
        onClick: r,
      },
      lo.default.createElement(oie, {
        className: zl.menuIcon,
      }),
    ),

    // 发送按钮
    lo.default.createElement(
      'button',
      {
        type: 'submit',
        disabled: !n.busy.value && !o,
        className: zl.sendButton,
        'data-permission-mode': e,
        onClick: (f) => {
          n.busy.value && !o && (f.preventDefault(), n.interrupt());
        },
      },
      d,
    ),
  );
}

// Mode 按钮组件
function DSt({ mode: n, onTap: e }) {
  switch (n) {
    case 'acceptEdits':
      return lo.default.createElement(
        'button',
        {
          type: 'button',
          className: zl.footerButton,
          onClick: e,
          title: 'Claude will edit your selected text or the whole file...',
        },
        lo.default.createElement($ye, null), // Fast forward icon
        lo.default.createElement('span', null, 'Edit automatically'),
      );

    case 'plan':
      return lo.default.createElement(
        'button',
        {
          type: 'button',
          className: zl.footerButton,
          onClick: e,
          title: 'Claude will explore the code and present a plan...',
        },
        lo.default.createElement(jye, null), // Pause icon
        lo.default.createElement('span', null, 'Plan mode'),
      );

    case 'bypassPermissions':
      return lo.default.createElement(
        'button',
        {
          type: 'button',
          className: zl.footerButton,
          onClick: e,
          title: 'Claude Code will not ask for your approval...',
        },
        lo.default.createElement(Uye, null), // Double chevron icon
        lo.default.createElement('span', null, 'Bypass permissions'),
      );

    case 'default':
    default:
      return lo.default.createElement(
        'button',
        {
          type: 'button',
          className: zl.footerButton,
          onClick: e,
          title: 'Claude will ask before each edit...',
        },
        lo.default.createElement(qye, null), // Pencil icon
        lo.default.createElement('span', null, 'Ask before edits'),
      );
  }
}

// Thinking 按钮组件
function RSt({ thinkingOn: n, toggleThinking: e }) {
  return lo.default.createElement(
    'button',
    {
      type: 'button',
      className: `${zl.menuButton} ${n ? zl.menuButtonActivated : zl.menuButtonInactivated}`,
      title: n ? 'Thinking on' : 'Thinking off',
      onClick: e,
    },
    // Thinking 图标 SVG
    lo.default.createElement(
      'svg',
      {
        width: '16',
        height: '16',
        viewBox: '0 0 16 16',
        fill: 'none',
        xmlns: 'http://www.w3.org/2000/svg',
      },
      lo.default.createElement('path', {
        d: 'M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915...',
        strokeWidth: '0.27',
        style: {
          stroke: 'var(--app-secondary-foreground)',
          fill: 'var(--app-secondary-foreground)',
        },
      }),
    ),
  );
}

关键发现:

  • Mode 切换逻辑 (4 种模式)
  • Thinking 开关状态管理
  • 忙碌状态显示不同图标
  • 使用 data-permission-mode 属性

D. ContentEditable 输入框

HTML 定位线索

  • 类名: .d, .fo
  • 属性: contenteditable="plaintext-only"

推断的实现模式

// ContentEditable 输入框 (从模式推断)
var c_ = {
  inputContainer: 'u',
  inputContainerBackground: 'Wr',
  messageInputContainer: 'fo',
  messageInput: 'd',
};

// 输入组件逻辑 (推断)
function MessageInput({ value, onChange, onSubmit }) {
  const inputRef = useRef(null);
  const [isComposing, setIsComposing] = useState(false);

  const handleInput = () => {
    if (inputRef.current) {
      const newValue = inputRef.current.textContent || '';
      onChange(newValue);
    }
  };

  const handleKeyDown = (e) => {
    // Enter 提交 (非 Shift+Enter)
    if (e.key === 'Enter' && !e.shiftKey) {
      if (e.nativeEvent.isComposing) return;
      e.preventDefault();
      onSubmit();
    }

    // Escape 取消
    if (e.key === 'Escape' && !e.metaKey && !e.ctrlKey) {
      e.preventDefault();
      // 取消操作
    }
  };

  useEffect(() => {
    if (inputRef.current && inputRef.current.textContent !== value) {
      inputRef.current.textContent = value;
    }
  }, [value]);

  return React.createElement(
    'div',
    {
      className: c_.messageInputContainer,
    },
    React.createElement('div', {
      ref: inputRef,
      className: c_.messageInput,
      contentEditable: 'plaintext-only',
      role: 'textbox',
      'aria-label': 'Message input',
      'aria-multiline': 'true',
      'data-placeholder': 'Ask Claude to edit…',
      onInput: handleInput,
      onKeyDown: handleKeyDown,
      spellCheck: false,
    }),
  );
}

四、事件处理逻辑映射

键盘快捷键汇总

快捷键 功能 组件
Escape 关闭菜单/对话框 所有弹窗
ArrowDown 下一项 命令菜单/文件选择
ArrowUp 上一项 命令菜单/文件选择
Enter 确认选择 命令菜单/文件选择
Tab 选择并继续 命令菜单/文件选择
Shift+Tab 切换模式 Footer 模式按钮
Enter (输入框) 发送消息 消息输入
Shift+Enter 换行 消息输入
/ 打开命令菜单 全局
@ @mentions 输入框

状态管理模式

// 全局 Session 状态 (推断)
const session = {
  busy: { value: boolean },
  thinkingLevel: { value: "off" | "default_on" },
  usageData: {
    value: {
      totalTokens: number,
      contextWindow: number
    }
  },
  interrupt: () => void,
  setThinkingLevel: (level) => void
}

五、完整的组件层级关系

App (根组件)
├── Header (.he)
│   ├── SessionButton (.E)
│   │   └── onClick: handleSessionsClick
│   ├── Spacer (.ke)
│   └── NewButton (.j)
│       └── onClick: handleNewSession
│
├── MainContent (.be > .Q > .ue > .ye)
│   ├── EmptyState (.Re)
│   └── MessageList (.M)
│
├── InputArea
│   ├── InputForm (.u)
│   │   ├── Background (.Wr)
│   │   ├── MessageInputContainer (.fo)
│   │   │   └── ContentEditable (.d)
│   │   └── Footer (.ri)
│   │       ├── ModeButton (.l)
│   │       ├── SelectionButton (.l)
│   │       ├── UsageIndicator
│   │       ├── ThinkingButton (.H)
│   │       ├── CommandMenuButton (.H)
│   │       └── SendButton (.r)
│   │
│   └── Popups (条件渲染)
│       ├── CommandMenu (.menuPopup)
│       │   ├── FilterInput (.filterInput)
│       │   └── CommandList (.commandList)
│       │       └── CommandItem (.commandItem)
│       │
│       ├── FileSelector ($u.menuPopup)
│       │   └── FileList ($u.fileList)
│       │       └── FileItem ($u.fileItem)
│       │
│       └── PermissionRequest (Ei.permissionRequestContainer)
│           ├── Content (Ei.permissionRequestContent)
│           ├── Options (Ei.buttonContainer)
│           └── RejectInput (Ei.rejectMessageInput)

六、可直接复用的代码模式

1. 键盘导航模式

// 通用键盘导航 Hook
const useKeyboardNavigation = (items, onSelect, onClose) => {
  const [selectedIndex, setSelectedIndex] = useState(0);

  const handleKeyDown = useCallback(
    (e) => {
      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault();
          setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
          break;
        case 'ArrowUp':
          e.preventDefault();
          setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
          break;
        case 'Enter':
        case 'Tab':
          if (!e.shiftKey) {
            e.preventDefault();
            onSelect(items[selectedIndex], e.key === 'Tab');
          }
          break;
        case 'Escape':
          e.preventDefault();
          onClose();
          break;
      }
    },
    [items, selectedIndex, onSelect, onClose],
  );

  useEffect(() => {
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [handleKeyDown]);

  return [selectedIndex, setSelectedIndex];
};

2. 自动滚动模式

// 滚动到选中项
const selectedRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  selectedRef.current?.scrollIntoView({
    behavior: 'instant',
    block: 'nearest',
  });
}, [selectedIndex]);

3. 防抖搜索模式

// 防抖加载列表
useEffect(() => {
  const timer = setTimeout(() => {
    fetchItems(searchQuery).then(setItems);
  }, 200);
  return () => clearTimeout(timer);
}, [searchQuery]);

七、总结与建议

成功提取的内容

  1. 完整的命令菜单组件逻辑 - 包含过滤、键盘导航、选择
  2. 文件选择器组件逻辑 - 包含搜索、预览、选择
  3. Footer 按钮逻辑 - 包含模式切换、状态管理
  4. 键盘导航模式 - 统一的导航逻辑
  5. 状态管理模式 - useState + useCallback 模式

🎯 可立即实现的组件

  1. ChatHeader - 参考 Footer 按钮模式
  2. CommandMenu - 完整代码已提取
  3. FileSelector - 完整代码已提取
  4. ContentEditable - 参考输入框模式

实施时间估算

  • ChatHeader: 2 小时
  • CommandMenu 移植: 3 小时
  • FileSelector 移植: 3 小时
  • 测试整合: 2 小时

总计: 1 天可完成核心组件


文档版本: v3.0 最后更新: 2025-11-18 状态: 已验证可行