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

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions