Skip to content

Feat: 新增 DropdownMenu 组件#58

Merged
qorzj merged 5 commits into
lessweb:mainfrom
hqwlkj:feature/dropdown-menu-refactor
May 14, 2026
Merged

Feat: 新增 DropdownMenu 组件#58
qorzj merged 5 commits into
lessweb:mainfrom
hqwlkj:feature/dropdown-menu-refactor

Conversation

@hqwlkj
Copy link
Copy Markdown
Contributor

@hqwlkj hqwlkj commented May 14, 2026

📋 概述

本次 PR 引入了通用的 DropdownMenu 组件,重构了技能和模型选择列表的实现,同时增强了会话系统消息支持和 UI 显示优化。

主要变更

  • ✅ 新增通用 DropdownMenu 组件,支持滚动、自定义渲染
  • ✅ 重构 Skills 和 Model 下拉菜单,消除重复代码
  • ✅ 添加 /model 命令的系统消息记录功能
  • ✅ 优化 Thinking 消息的 UI 显示(增加缩进和 reasoning effort 显示)
  • ✅ 新增 calculateVisibleStart 函数的完整单元测试(17 个测试用例)
  • ✅ 修复会话切换后欢迎页不显示的问题

🎯 变更详情

1. 新增通用 DropdownMenu 组件

文件: src/ui/DropdownMenu.tsx (新建)

核心特性

  • 通用数据驱动设计: 接受 DropdownMenuItem[] 数组,而非硬编码的 JSX
  • 智能滚动窗口: 自动计算可见区域,确保活跃项始终可见
  • 自定义渲染支持: 通过 renderItem prop 支持完全自定义渲染
  • 性能优化: 使用 React.memouseMemo 避免不必要的重渲染
  • 标签列自适应宽度: 根据内容自动计算最佳列宽

关键函数

export function calculateVisibleStart(
  activeIndex: number,
  totalItems: number,
  maxVisible: number
): number

计算滚动窗口的起始位置,确保活跃项居中显示(当空间允许时)。

Props 接口

type DropdownMenuProps = {
  items: DropdownMenuItem[];           // 数据项列表
  activeIndex: number;                 // 当前活跃项索引
  maxVisible?: number;                 // 最大可见项数(默认 8)
  width: number;                       // 容器宽度
  title?: string;                      // 标题
  titleColor?: string;                 // 标题颜色
  activeColor?: string;                // 活跃项颜色
  helpText?: string;                   // 帮助文本
  emptyText?: string;                  // 空列表文本
  renderItem?: (item, isActive) => React.ReactNode;  // 自定义渲染器
};

2. 重构 Skills 和 Model 选择菜单

文件: src/ui/PromptInput.tsx

变更前

Skills 和 Model 菜单各自独立实现,存在大量重复代码:

  • 重复的边框、滚动指示器逻辑
  • 重复的键盘导航处理
  • 重复的渲染逻辑

变更后

统一使用 DropdownMenu 组件:

<DropdownMenu
  title="Skills"
  helpText="↑↓ to navigate • Enter to toggle • Esc to close"
  items={skillsDropdownItems.map((skill) => ({
    key: skill.name,
    label: skill.name,
    description: skill.description,
    selected: isSkillSelected(skill.name),
    statusIndicator: skill.isLoaded
      ? { symbol: "✓", color: "green" }
      : undefined,
  }))}
  activeIndex={skillsDropdownIndex}
  activeColor="#229ac3"
  maxVisible={6}
/>

优势:

  • 📉 代码行数减少 ~50 行
  • 🔄 逻辑复用,易于维护
  • 🎨 统一的视觉风格
  • ⚡ 更好的性能(React.memo)

3. 增强 /model 命令的系统消息支持

文件: src/ui/App.tsx, src/session.ts

功能

当用户执行 /model 命令切换模型时,会在聊天历史中添加一条系统消息:

/model
⎿ Set model to gpt-4o

实现细节

  1. 新增 addSessionSystemMessage 方法 (src/session.ts)

    addSessionSystemMessage(
      sessionId: string,
      content: string,
      meta?: MessageMeta
    ): void
  2. MessageMeta 扩展

    interface MessageMeta {
      isModelChange?: boolean;
      modelConfig?: {
        model: string;
        thinkingEnabled: boolean;
        reasoningEffort: string;
      };
    }
  3. UI 显示优化 (src/ui/MessageView.tsx)

    • Thinking 消息增加左侧缩进(marginLeft={2}
    • 显示 reasoning effort 信息:(normal effort), (high effort), 等

4. 修复会话切换后欢迎页不显示的问题

文件: src/ui/App.tsx

问题

执行 /resume 切换会话后,欢迎页和历史记录都不显示。

根本原因

<Static> 组件使用增量渲染机制,切换会话时不会重新挂载,导致:

  1. 旧的 items 仍然保留在终端中
  2. 新的 welcomeItem 不会被渲染

解决方案

使用 key 机制强制 <Static> 重新挂载:

const [staticRemountKey, setStaticRemountKey] = useState(0);

// 切换会话时
const newMessages = loadVisibleMessages(sessionManager, sessionId);
setMessages(newMessages);              // 同时设置消息
setStaticRemountKey((k) => k + 1);     // 触发重新挂载
<Static key={staticRemountKey} items={staticItems}>
  {(item) => {
    if (item.id.startsWith("__welcome__")) {
      return <WelcomeScreen ... />;
    }
    return <MessageView ... />;
  }}
</Static>

优势:

  • ✅ 切换会话后欢迎页正确显示
  • ✅ 历史记录完整加载
  • ✅ 终端 resize 时正确重绘
  • ✅ 无闪烁,无残留内容

5. 新增单元测试

文件: src/tests/dropdownMenu.test.ts (新建)

calculateVisibleStart 函数编写了 17 个测试用例,覆盖:

测试场景

类别 测试用例数 覆盖内容
基本场景 6 居中、开头、末尾、少于 maxVisible、单项、空列表
边界情况 5 奇偶 maxVisible、精确边界、中间范围
特殊参数 3 超大 maxVisible、activeIndex 越界、maxVisible=1
滚动行为 3 向下滚动、向上滚动的完整流程

测试结果

# tests 184
# pass 184
# fail 0

🧪 测试验证

自动化测试

npm test
# ✅ 184 tests passed
# ✅ 0 tests failed

手动测试清单

  • /skills 命令 - Skills 下拉菜单正常显示和滚动
  • /model 命令 - Model 下拉菜单正常显示和滚动
  • /model 命令 - 切换后聊天历史中出现系统消息
  • /resume 命令 - 切换会话后欢迎页正确显示
  • /resume 命令 - 切换会话后历史记录正确加载
  • 终端 resize - 欢迎页和历史记录正确重绘
  • Thinking 消息 - 显示缩进和 reasoning effort
  • 长列表滚动 - 上下滚动时活跃项始终可见

🎨 UI/UX 改进

下拉菜单统一风格

  • 圆角边框容器(borderStyle="round"
  • 标题栏分隔线
  • 滚动指示器(… 3 above, … 5 more
  • 帮助文本底部边框
  • 统一的活跃项高亮颜色(#229ac3

🚀 性能优化

  1. React.memo: DropdownMenu 组件使用 memo 包装,避免不必要的重渲染
  2. useMemo: labelColumnWidth 计算使用 memo 优化
  3. 数据驱动: 从 JSX 数组改为数据对象数组,减少虚拟 DOM 差异计算
  4. 增量渲染: 保持 <Static> 的增量渲染优势,仅在必要时重新挂载


✅ PR 检查清单

  • 代码通过 TypeScript 类型检查 (npm run typecheck)
  • 代码通过 ESLint 检查 (npm run lint)
  • 代码通过 Prettier 格式化 (npm run format:check)
  • 所有测试通过 (npm test - 184 tests)
  • 构建成功 (npm run build - 293.1kb)
  • 无控制台警告或错误
  • 代码注释完整
  • 遵循项目代码规范

hqwlkj added 5 commits May 13, 2026 17:26
- 新增DropdownMenu组件,支持滚动、选中指示与自定义渲染
- 使技能选择区使用DropdownMenu替代手写列表,统一样式与行为
- 模型选择区同样改用DropdownMenu组件,简化代码
- 添加DropdownMenu相关类型定义和滚动窗口计算函数
- 优化PromptInput中颜色和显示逻辑,提升一致性和可读性
- 修正session模块中fs.readdirSync调用的变量初始化问题
- 在模型配置变更时生成系统角色消息,包含模型设置详情
- 将模型变更消息添加至消息列表,支持消息追踪
- 在消息视图中新增模型变更消息渲染逻辑,展示模型名称和推理强度
- 调整用户消息视图布局,优化水平排列和间距
- 修改提示输入组件下拉菜单最大可见项数为6,提升视觉体验
- 扩展SessionMessage类型,支持模型变更相关元信息
- 新增单元测试覆盖DropdownMenu的可见起始项计算逻辑
- 新增 addSessionSystemMessage 方法,用于统一添加系统消息
- 优化 App.tsx 中模型切换逻辑,调用 sessionManager 添加系统消息
- 保持无活动会话时依旧能将消息追加到本地消息列表
- 重构消息创建流程,抽取 meta 和 content 变量提高代码清晰度
- 确保系统消息包含准确的时间戳及元数据配置
@qorzj qorzj merged commit b242190 into lessweb:main May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants