Skip to content

phase-10-keybinding-system #720

Description

@pionxe

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+EnterEnter 不可区分,此时请使用 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 — 为什么全新键位系统

  1. v1 键位不可兼容:v1 的 Esc=聚焦输入、Ctrl+J=换行、Ctrl+W=取消运行,与 v2 的 Esc=Normal Mode、Shift+Enter=换行、Ctrl+C=取消 冲突 —— 无法在同一套 keyMap 共存
  2. Vim 用户直觉:三层模式是 Vim/Neovim 用户熟悉的操作模型,降低学习成本
  3. 输入与导航分离:打字时不会被 j/k 滚动干扰,导航时不会误输入字符

How — 怎么做

  1. 创建 internal/tuiv2/keymap/keys.go,定义三种模式的按键映射(Action 枚举 + Match 函数)
  2. 使用 Bubble Tea 的 key.Binding 类型定义每个按键绑定
  3. App.Update() 中根据 ViewState.Mode 路由按键到不同 handler(分层约定:模式切换键在 app 层拦截,编辑键委派 prompt 组件)
  4. 实现 handleInputModeKey(), handleNormalModeKey(), handleLeaderKey()
  5. Leader Mode 实现为有限状态机:Space → 等待下一个按键 → 执行动作 → 回到 Normal;超时(1s)或非后缀键立即静默回 Normal
  6. :q / :debug / :compact / :mode 等命令通过独立的 components/cmdline.go 组件处理(底部单行输入),/ 搜索同理
  7. Overlay 类型用 typed 常量(OverlayType)收敛,禁止散落字符串字面量
  8. 不复用 v1 的 keyMap,不 import internal/tui/

验收标准

  • Esc 从 Input Mode 切换到 Normal Mode
  • Normal Mode 下 i / Enter 回到 Input Mode
  • Normal Mode 下 j/k 滚动 Agent Stream
  • Normal Mode 下 g 滚动到顶部、G 滚动到底部(单键)
  • Normal Mode 下 Ctrl+F / Ctrl+B 整页翻页
  • Space 进入 Leader Mode,按下后缀键后执行动作并回到 Normal
  • Leader Mode 非后缀键或超时 → 立即静默回到 Normal Mode
  • Ctrl+C 双退逻辑:首次取消运行,再次(2 秒窗口内)退出程序
  • Input Mode 空 Ctrl+D 退出程序(EOF);非空删除光标后字符
  • :q 退出程序、:debug 切换调试、:compact / :mode 可用
  • /query 搜索:回车跳转首个匹配,n / N 循环跳转,Esc 清除
  • 搜索结果在 stream 增长后标记 Stale 提示
  • 底部状态行模式指示正确切换(input=FG / normal=Subtle / leader=Accent 粗体)
  • Overlay 使用 typed 常量,无硬编码字符串字面量
  • app.go 及拆分文件均 < 800 行
  • go test ./internal/tuiv2/... 覆盖各模式转换与全部新逻辑,覆盖率 100%

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions