Phase 10: 全新快捷键系统
- Phase: 10
- 优先级: P1
- 依赖: Phase 9(Command Prompt 可用)
What — 要做什么
实现 TUI v2 的三层键位系统:Input Mode、Normal Mode、Leader Key。完全独立于 TUI v1 的键位系统。
三层模式
┌──────────────┐
默认进入 │ Input Mode │
┌───────────────▶│ (打字/发送) │
│ └──────┬───────┘
│ │ Esc
│ ▼
│ ┌──────────────┐
│ i / Enter │ Normal Mode │
│ ◀────────────│ (导航/命令) │
│ └──┬───────┬───┘
│ │ │ Space
│ │ ▼
│ │ ┌──────────────┐
│ │ │ Leader Mode │
│ │ │ (Space + key) │
│ │ └──────────────┘
│ │ │
│ │ key pressed
│ │ (自动回到 Normal)
│ │ │
└───────────────────┴───────┘
Input Mode 键位
| 按键 |
行为 |
| 可打印字符 |
输入文本 |
Enter |
发送消息 / 确认 |
Shift+Enter |
换行 |
Backspace |
删除前一个字符 |
Ctrl+C |
取消当前运行(首次),再次退出(双退) |
Ctrl+D |
退出(EOF) |
Esc |
进入 Normal Mode |
Ctrl+A / Ctrl+E |
行首 / 行尾 |
Ctrl+K |
删除到行尾 |
Ctrl+W |
删除前一个词 |
Ctrl+D 在 Input Mode 下按上下文区分:输入框为空时退出程序(EOF);输入框非空时删除光标后一个字符(Readline 语义)。Normal Mode 下 Ctrl+D 为半页下翻,两者由模式隔离无歧义。
Shift+Enter 终端兼容性:仅在支持 kitty keyboard protocol 的终端(Kitty / WezTerm / Alacritty 等)可与 Enter 区分;其余终端(GNOME Terminal、tmux、screen、VS Code 终端等)Shift+Enter 与 Enter 不可区分,此时请使用 Ctrl+J 作为换行替代(已支持)。
Normal Mode 键位
| 按键 |
行为 |
i / Enter |
进入 Input Mode |
j / Down |
向下滚动一行 |
k / Up |
向上滚动一行 |
g |
滚动到顶部 |
G |
滚动到底部 |
Ctrl+D / Ctrl+U |
向下/上翻半页 |
Ctrl+F / Ctrl+B |
向下/上翻一页 |
Space |
进入 Leader Mode |
: q Enter |
退出 |
: debug Enter |
切换调试模式 |
/ |
搜索(stream 内) |
n / N |
搜索结果下一个 / 上一个(循环) |
Leader Mode 行为补充:按下非后缀键或超时(1 秒)时,立即回到 Normal Mode 并取消计时器,静默处理(符合 Vim Leader 习惯,模式指示器从 [leader] 变回 [normal] 即视觉反馈)。
Leader Key 键位(Space 后)
| 按键 |
行为 |
Space p |
命令面板 |
Space n |
新建会话 |
Space s |
会话选择器 |
Space m |
模型选择器 |
Space h |
帮助 |
Space r |
重试上次运行 |
Space c |
取消当前运行 |
Space Space |
切换到上一个会话 |
按下 Leader key 后的按键后自动回到 Normal Mode(除非打开的面板保持打开)。
搜索语义
Normal Mode 下 / 进入搜索输入(底部 /query 单行),回车执行搜索:
- 全量扫描
state.Stream(append-only,一次性 O(n) 扫描),按子串匹配(忽略大小写)记录匹配 entry 的全局索引到 Search.Matches。
- 由于
state.Stream 是 append-only(只在末尾追加,无中间插入),匹配索引永远有效,不会因新内容插入而错乱。
n / N 在匹配间循环跳转(到末尾折返首位),基于全局索引计算滚动偏移(O(1))。
- 快照 + Stale 指示器:回车时固定
Matches(快照语义),流式新增内容不自动加入 Matches。当 stream 增长后,Search.Stale=true,UI 提示"results may be stale — press / to refresh"。用户重新执行搜索时清除 Stale。
Esc 清除整个搜索状态(Query / Matches / MatchIndex / Stale)。
- 切出 Normal Mode(进 Input / Leader)时自动清除搜索状态。
- 空查询回车为 no-op(关闭搜索输入,不执行扫描)。
- 无匹配时提示"no matches",
n / N 在无匹配时为 no-op。
搜索最小集:本 Phase 不实现匹配高亮渲染、正则搜索、大小写切换;这些留待后续 Phase。当前仅做"输入 → 扫描 → 跳转 → stale 提示 → 清除"。
模式切换机制
模式切换通过同步修改 ViewState.Mode 完成。App.Update() 在对应 handler(handleInputModeKey / handleNormalModeKey / handleLeaderKey)内直接修改 a.state.Mode,并同步清理相关子状态(如切出 Normal Mode 时清除搜索状态、Ex 输入),底部状态行直接读取 state.Mode 渲染。
不引入额外的 tea.Msg(如 ModeChangedMsg)。理由:Bubble Tea 的同步 model-update-view 循环里,直接改 model 字段是唯一正确且无歧义的方式;额外的模式切换消息在当前架构下没有消费者(无日志器、无 undo 栈、无外部订阅组件),属于过度设计。未来若出现真实消费者(如独立状态栏组件、模式切换审计),再在切 mode 处补发消息即可,成本可控。
遵循 AGENTS.md「代码为准」原则:当文档描述与代码实现冲突时,以代码实现为准并同步更新文档——本节即据此同步。
模式指示器
底部状态行左侧:
[input] — Input Mode(FG 色,BaseStyle)
[normal] — Normal Mode(Subtle 色)
[leader] — Leader Mode(Accent 色,加粗;不加闪烁以避免视觉干扰,加粗已足够区分)
Why — 为什么全新键位系统
- v1 键位不可兼容:v1 的
Esc=聚焦输入、Ctrl+J=换行、Ctrl+W=取消运行,与 v2 的 Esc=Normal Mode、Shift+Enter=换行、Ctrl+C=取消 冲突 —— 无法在同一套 keyMap 共存
- Vim 用户直觉:三层模式是 Vim/Neovim 用户熟悉的操作模型,降低学习成本
- 输入与导航分离:打字时不会被
j/k 滚动干扰,导航时不会误输入字符
How — 怎么做
- 创建
internal/tuiv2/keymap/keys.go,定义三种模式的按键映射(Action 枚举 + Match 函数)
- 使用 Bubble Tea 的
key.Binding 类型定义每个按键绑定
- 在
App.Update() 中根据 ViewState.Mode 路由按键到不同 handler(分层约定:模式切换键在 app 层拦截,编辑键委派 prompt 组件)
- 实现
handleInputModeKey(), handleNormalModeKey(), handleLeaderKey()
- Leader Mode 实现为有限状态机:
Space → 等待下一个按键 → 执行动作 → 回到 Normal;超时(1s)或非后缀键立即静默回 Normal
:q / :debug / :compact / :mode 等命令通过独立的 components/cmdline.go 组件处理(底部单行输入),/ 搜索同理
- Overlay 类型用 typed 常量(
OverlayType)收敛,禁止散落字符串字面量
- 不复用 v1 的
keyMap,不 import internal/tui/
验收标准
Phase 10: 全新快捷键系统
What — 要做什么
实现 TUI v2 的三层键位系统:Input Mode、Normal Mode、Leader Key。完全独立于 TUI v1 的键位系统。
三层模式
Input Mode 键位
EnterShift+EnterBackspaceCtrl+CCtrl+DEscCtrl+A/Ctrl+ECtrl+KCtrl+WNormal Mode 键位
i/Enterj/Downk/UpgGCtrl+D/Ctrl+UCtrl+F/Ctrl+BSpace: q Enter: debug Enter/n/NLeader Key 键位(
Space后)Space pSpace nSpace sSpace mSpace hSpace rSpace cSpace Space按下 Leader key 后的按键后自动回到 Normal Mode(除非打开的面板保持打开)。
搜索语义
Normal Mode 下
/进入搜索输入(底部/query单行),回车执行搜索:state.Stream(append-only,一次性 O(n) 扫描),按子串匹配(忽略大小写)记录匹配 entry 的全局索引到Search.Matches。state.Stream是 append-only(只在末尾追加,无中间插入),匹配索引永远有效,不会因新内容插入而错乱。n/N在匹配间循环跳转(到末尾折返首位),基于全局索引计算滚动偏移(O(1))。Matches(快照语义),流式新增内容不自动加入Matches。当 stream 增长后,Search.Stale=true,UI 提示"results may be stale — press / to refresh"。用户重新执行搜索时清除 Stale。Esc清除整个搜索状态(Query / Matches / MatchIndex / Stale)。n/N在无匹配时为 no-op。模式切换机制
模式切换通过同步修改
ViewState.Mode完成。App.Update()在对应 handler(handleInputModeKey/handleNormalModeKey/handleLeaderKey)内直接修改a.state.Mode,并同步清理相关子状态(如切出 Normal Mode 时清除搜索状态、Ex 输入),底部状态行直接读取state.Mode渲染。不引入额外的
tea.Msg(如ModeChangedMsg)。理由:Bubble Tea 的同步 model-update-view 循环里,直接改 model 字段是唯一正确且无歧义的方式;额外的模式切换消息在当前架构下没有消费者(无日志器、无 undo 栈、无外部订阅组件),属于过度设计。未来若出现真实消费者(如独立状态栏组件、模式切换审计),再在切 mode 处补发消息即可,成本可控。遵循 AGENTS.md「代码为准」原则:当文档描述与代码实现冲突时,以代码实现为准并同步更新文档——本节即据此同步。
模式指示器
底部状态行左侧:
[input]— Input Mode(FG 色,BaseStyle)[normal]— Normal Mode(Subtle 色)[leader]— Leader Mode(Accent 色,加粗;不加闪烁以避免视觉干扰,加粗已足够区分)Why — 为什么全新键位系统
Esc=聚焦输入、Ctrl+J=换行、Ctrl+W=取消运行,与 v2 的Esc=Normal Mode、Shift+Enter=换行、Ctrl+C=取消 冲突 —— 无法在同一套 keyMap 共存j/k滚动干扰,导航时不会误输入字符How — 怎么做
internal/tuiv2/keymap/keys.go,定义三种模式的按键映射(Action 枚举 + Match 函数)key.Binding类型定义每个按键绑定App.Update()中根据ViewState.Mode路由按键到不同 handler(分层约定:模式切换键在 app 层拦截,编辑键委派 prompt 组件)handleInputModeKey(),handleNormalModeKey(),handleLeaderKey()Space→ 等待下一个按键 → 执行动作 → 回到 Normal;超时(1s)或非后缀键立即静默回 Normal:q/:debug/:compact/:mode等命令通过独立的components/cmdline.go组件处理(底部单行输入),/搜索同理OverlayType)收敛,禁止散落字符串字面量keyMap,不 importinternal/tui/验收标准
Esc从 Input Mode 切换到 Normal Modei/Enter回到 Input Modej/k滚动 Agent Streamg滚动到顶部、G滚动到底部(单键)Ctrl+F/Ctrl+B整页翻页Space进入 Leader Mode,按下后缀键后执行动作并回到 NormalCtrl+C双退逻辑:首次取消运行,再次(2 秒窗口内)退出程序Ctrl+D退出程序(EOF);非空删除光标后字符:q退出程序、:debug切换调试、:compact/:mode可用/query搜索:回车跳转首个匹配,n/N循环跳转,Esc清除app.go及拆分文件均 < 800 行go test ./internal/tuiv2/...覆盖各模式转换与全部新逻辑,覆盖率 100%