feat(tuiv2): Phase 10 — 全新快捷键系统(三层键位/命令行/搜索)#730
Conversation
为三层键位系统补充 Phase 10 规划所需的动作与映射: - Input Mode 新增编辑动作:ActionLineStart/LineEnd/KillLine/DeleteWord 分别绑定 Ctrl+A(行首)/Ctrl+E(行尾)/Ctrl+K(删到行尾)/Ctrl+W(删前一词) - Normal Mode 新增整页翻页动作:ActionFullPageDown/FullPageUp 绑定 Ctrl+F/Ctrl+B;g 保持单键到顶、G 到底 - Leader 新增动作:ActionLeaderRetry/CancelRun/LastSession/ModelPicker 供 Step 7 改造 Leader handler 使用 - MatchInputKey 增加 ctrl+a/e/k/w 映射;ctrl+d 不映射 (由 app 层按输入框空否决定 EOF/DeleteChar),并补充注释说明 - Help 文案同步更新:Ctrl+A/E/K/W、Ctrl+F/B、Space r/Space Space LeaderHelp 标注 compact/toggle-mode 迁移提示 - 校验帮助文案不再出现 "g g" 双键描述 注:ActionLeaderToggleMode/Compact 暂保留(app.go 仍引用), Step 7 改造 Leader handler 时统一删除。
收敛浮层字面量为类型化常量,并为搜索/命令行功能预留状态字段:
- 新增 OverlayType 类型与 OverlayNone/Palette/Help/SessionPicker/
ModelPicker/Confirm/Ex/Search 常量,消除散落在 app.go 与各组件
中的字符串字面量(符合 AGENTS.md "不硬编码业务语义字符串")
- OverlayState.Active 由 string 改为 OverlayType,openOverlay
签名同步调整,编译器保证引用完整
- 新增 SearchState{Active,Query,Matches,MatchIndex,Stale}:
Matches 存放 Stream 全局索引(append-only 保证稳定),Stale
在 stream 增长后置位提示用户重新搜索
- 新增 ExState{Active,Input} 供 : 命令行输入使用
- reducer.cloneViewState 增加 Search.Matches 深拷贝,避免共享切片
- 同步更新所有组件与测试文件中的 overlay 字面量引用
抽离 Normal Mode 下的命令行与搜索输入逻辑为独立组件,避免 app.go 膨胀:
- 新增 CmdLine 组件(tea.Model),处理 Ex(:) 与 Search(/) 输入:
- 字符追加、Backspace、Enter 提交、Esc/Ctrl+C 取消
- 导航键(j/k 等)在输入时忽略,不触发滚动
- 通过返回 ExCommandMsg/SearchSubmitMsg/CmdLineCancelMsg 让 app 层
执行副作用,组件本身无副作用,职责单一
- RunSearch 全量扫描 Stream 返回匹配 entry 全局索引(append-only
保证索引稳定),大小写不敏感,空查询返回 nil
- View 渲染 : 或 / 前缀输入行 + 光标,搜索结果过时时显示 stale 提示
- 测试覆盖:Ex 输入/提交/取消、Search 输入/提交、View 渲染(含 stale)、
RunSearch 匹配/空查询/无匹配
cmdline 组件先于 prompt 改造完工(plan-v4 修正后的顺序),后续
Step 5 prompt 将委托渲染给它。
为 Input Mode 补充 Readline 风格行编辑,并按模式着色状态行指示: - handleInputKey 新增 Ctrl+A(行首)/Ctrl+E(行尾)/Ctrl+K(删到行尾)/ Ctrl+W(删前一词) 编辑动作 - 抽取 killToEnd() 与 deleteWordBack() 辅助函数:Ctrl+W 按空格与 常见标点分词,符合 Readline 语义 - modeLine 左侧模式指示按模式着色:input=BaseStyle(FG)、 normal=SubtleStyle、leader=AccentStyle 加粗(不加闪烁), 右侧会话/模型信息保持 SubtleStyle - 新增 modeIndicatorStyle 辅助函数收敛样式选择 注:esc/i 的模式切换分支暂保留,Step 8 app 层接管后再迁移删除。 测试覆盖 Ctrl+A/E/K/W 全流程与三种模式指示配色。
Normal Mode 下补充整页翻页能力(vim Ctrl+F/Ctrl+B 语义): - AgentStream.Update 新增 ctrl+b(整页上翻)/ctrl+f(整页下翻), 步长为 visibleLineCount(整页),复用 clampScroll 与 AutoScroll 逻辑,与现有 ctrl+u/ctrl+d(半页) 一致 - 空流下 ctrl+f/b 经 clampScroll(maxOffset=0) 保护,不崩溃 - 测试覆盖:底部 Ctrl+B 上翻、Ctrl+F 下翻、空流边界
按 Phase 10 规划调整 Leader 后缀绑定,并实现新动作: - Leader m 由 toggle Agent mode 改为打开 model_picker(规划要求) - Leader c 由 manual compact 改为取消当前运行(规划要求), 抽 cancelCurrentRun 辅助:运行中返回 cancel cmd,空闲静默 no-op - 新增 Leader r:重试上次运行(retryLastRun),用 lastUserText 重发, 无历史时提示 "No previous run to retry" - 新增 Leader Space:切换上一会话(switchToLastSession),用 prevSessionID, 无上一会话时提示 "No previous session to switch" - App 新增私有字段 lastUserText/prevSessionID(不入 ViewState): handleSubmitMessage 记录 lastUserText,handleSessionSelect 切换前 存当前会话到 prevSessionID - Leader 非后缀键静默回 Normal(独占捕获,不泄漏给 Normal handler) - 删除已废弃的 ActionLeaderToggleMode/ActionLeaderCompact 常量, MatchLeaderKey 与测试同步更新 - 测试覆盖 m/c(运行+空闲)/r(有+无历史)/space(有+无上一会话)/非后缀键 compact 与 toggle-mode 功能保留在 palette(:compact / :mode), LeaderHelp 已在 Step 1 标注迁移提示。
完成 Normal/Input 模式按键分层路由与命令行/搜索功能整合: - handleInputModeKey 拦截 Ctrl+D 按上下文分发:输入空→EOF 退出程序, 非空→删除光标后字符(委派 prompt);Ctrl+D 不进 MatchInputKey - handleNormalModeKey 新增::→打开 Ex overlay、/→打开 Search overlay、 n/N→搜索匹配循环跳转、Ctrl+F/B 整页路由 stream - Update 新增消息处理:ExCommandMsg(执行:命令并关 overlay)、 SearchSubmitMsg(全量扫描+跳首个)、CmdLineCancelMsg(Esc 取消) - overlay 为 Ex/Search 时按键路由给 cmdLine 组件 - gatewayEventMsg: stream 增长时标记 Search.Stale=true; RunStarted 时清理 Search/Ex 子状态 - View: Ex/Search overlay 时用 cmdLine.View()+modeLine 替代 prompt 输入区 - 新增辅助: enterInputFromNormal/clearSearchAndEx/openEx/openSearch/ executeExCommand/executeSearch/jumpSearchMatch/scrollToStreamIndex - prompt.handleInputKey 删除 esc/i 模式切换分支(已由 app 层接管), prompt 只保留编辑;新增导出 ModeLine() 供 app 复用状态行 - 测试覆盖: Ctrl+D 三态、Ex 命令(q/debug/help/compact/mode/未知)、 Ex/Search overlay 流程、搜索提交+n/N+空查询+无匹配、stream 增长 stale 注: app.go 当前 1405 行,下一步拆分为 app_normal/app_leader 等 回到 <800 行(plan-v4 Step 8 拆分部分)。
app.go 内联路由/命令行/搜索后膨胀至 1405 行,违反 AGENTS.md 单文件 <800 行约束,按职责拆分为同包多文件: - app.go (785行):保留 App 结构、Init/Update/View、handleKeyMsg/ handleInputModeKey/handleCtrlC、toggle/trigger 辅助、appendStream/ bindComponents 等核心装配 - app_normal.go (215行):handleNormalModeKey + Ex/Search/n/N 辅助 (enterInputFromNormal/openEx/openSearch/clearSearchAndEx/ executeExCommand/executeSearch/jumpSearchMatch/scrollToStreamIndex) - app_leader.go (132行):handleLeaderKey + cancelCurrentRun/ retryLastRun/switchToLastSession - app_commands.go (254行):Gateway 客户端 tea.Cmd 工厂与消息类型 (loadInitialCmd/submitMessageCmd/cancelRunCmd/loadSessionCmd/ createSessionCmd 等 + initialLoadedMsg/gatewayEventMsg 等) - app_view.go (147行):视图布局与尺寸适配 (applyWindowSize/mainArea/separatorLine/fitViewToTerminal/fitLine/ debugLine/applyInitialLoaded) 纯代码移动,无逻辑变更,全量测试通过。
在实施指南末尾追加"后续 Phase 进展"段落,记录 tui-v2-issues 规划下 已交付的 Phase 9(交互增强)与 Phase 10(全新快捷键系统): - Phase 10 三层键位补全:Input 行编辑、Normal 整页翻页与 stream 搜索、 : 命令行、Leader 动作调整、模式指示配色、Ctrl+D 上下文分发、 Overlay typed 常量 - 指向规划文档 phase-10-keybinding-system.md 查阅详细键位与验收标准 - 注明 Phase 10 不引入 ModeChangedMsg(遵循 AGENTS.md 代码为准原则)
依据 tmux 1/2 Claude Code 第 1 轮审查(两方一致结论)修复: 必须修复: - 删除未映射的 ActionSearchBackward 常量(? 键无消费者,原 Normal 下 ? 打开 help 的行为已由 Space h / :help 覆盖,保留常量是死代码/退化) - scrollToStreamIndex 删除不可能触发的 estimated<0 死代码分支 (targetIndex 已被上方 guard 限制在 [0,len),estimated 恒 >0) - 清理拆分残留的重复/孤儿注释(cancelCurrentRun 重复注释、 app_normal.go 中 handleLeaderKey 孤儿注释) 测试盲区补齐(增量覆盖率≥80% 硬性要求): - CmdLine.Init() 0%→100%、handleBackspace 60%→100%(Search overlay Backspace) - CommandPrompt.ModeLine() 0%→100%(导出方法) - cancelCurrentRun 71.4%→100%(client==nil 分支) - scrollToStreamIndex 71.4%→100%(负数/越界/空流边界) - leaderTimeoutCmd 50%→100%(超时回退 + 非 Leader no-op) - loadInitialCmd 失败分支(离线/关闭 client) - 空 stream 搜索、空 ex 命令边界 覆盖率提升:app 包 90.6%→92.0%,components 89.7%→90.0%。
依据 tmux 1/2 Claude Code 第 2 轮复审(两方一致结论)修复: - 清理 app.go 拆分残留的重复注释块(23 处):app_normal.go(7)、 app_leader.go(2)、app_commands.go(10)、app_view.go(4),函数注释 不再重复出现两次 - 删除 keymap 中未被 app 层消费的 4 个行编辑 Action: ActionLineStart/LineEnd/KillLine/DeleteWord 及其 ctrl+a/e/k/w 映射 (这些编辑键由 prompt 组件内部直接处理,保留 keymap Action 会造成 架构不一致——keymap 定义了但无人消费);MatchInputKey 注释补充说明 编辑键归属 prompt 层以保持内聚 - TestLoadInitialCmdErrorPaths 阻塞风险经实测确认为假阳性 (fakegateway Close 后 Health 返回 error 而非挂起,15s 超时测试通过) 全量测试通过,覆盖率保持。
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
| a.state.Layout.AutoScroll = false | ||
| // 反向估算:偏移越大表示越靠顶部。目标越靠后(索引大)越接近底部,偏移越小。 | ||
| // targetIndex 已被上方 guard 限制在 [0, len),故 estimated 恒 > 0,无需再判负。 | ||
| a.state.Layout.ScrollOffset = len(a.state.Stream) - targetIndex |
There was a problem hiding this comment.
This offset uses the stream entry index as if it were a rendered-line offset, and it is off by one even for one-line entries. AgentStream.visibleLines treats ScrollOffset as the number of rendered lines hidden below the viewport (end := len(lines) - ScrollOffset), so with 20 one-line entries and a 4-line viewport, jumping to match index 19 sets offset 1 and renders entries 15-18, leaving the matched last entry just below the viewport. Multi-line messages, timestamps, and separators make the mismatch larger. Search navigation should map the target entry to its rendered line position, or expose a stream helper that scrolls to an entry and accounts for the renderer's line model.
| switch c.state.Overlay.Active { | ||
| case state.OverlayEx: | ||
| if len(c.state.Ex.Input) > 0 { | ||
| c.state.Ex.Input = c.state.Ex.Input[:len(c.state.Ex.Input)-1] |
There was a problem hiding this comment.
Backspace is slicing one byte, not one rune. If the user types a non-ASCII search term or command argument, for example 错, the first backspace leaves an invalid UTF-8 string (错 is three bytes), so the prompt/search query renders as replacement characters and the submitted query can be corrupted. The main prompt already edits []rune; this cmdline path should do the same for both Ex.Input and Search.Query.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
概述
实现 TUI v2 的 Phase 10:全新快捷键系统。在已有的三层模式骨架(Input / Normal / Leader)上补全规划文档要求的全部能力,并完成按键分层路由、命令行、stream 搜索、模式指示配色、app.go 拆分。
Closes #720
新增能力
Input Mode(行编辑)
Ctrl+A/Ctrl+E— 行首 / 行尾Ctrl+K— 删除到行尾(killToEnd)Ctrl+W— 删除前一个词(deleteWordBack,按空格/标点分词)Ctrl+D— 上下文分发:输入为空时 EOF 退出;非空时删除光标后字符Esc— 进入 Normal Mode(由 app 层拦截,不泄漏给 prompt)Normal Mode(导航与命令)
g到顶(单键)、G到底Ctrl+F/Ctrl+B— 整页下翻 / 上翻:— 命令行输入:q/quit/exit(退出)、debug、help、compact、mode、未知命令提示/— 搜索:回车全量扫描 stream(append-only,索引稳定)、n/N循环跳转、Esc清除、stream 增长标记Stale提示Leader Mode(
Space后)Space m— 模型选择器(原 toggle mode 迁移到:mode)Space c— 取消当前运行(原 compact 迁移到:compact)Space r— 重试上次运行(无历史时提示)Space Space— 切换上一会话(无上一会话时提示)p/n/s/h/q/f/l视觉与状态
[input]=FG、[normal]=Subtle、[leader]=Accent 加粗OverlayType)收敛,消除字符串字面量架构改进
按键分层约定:
模式切换键在 app 层拦截,不传 prompt;
prompt.handleInputKey删除 esc/i 分支,只保留编辑。app.go 拆分(满足 <800 行):
app.go(785)— App 结构、Init/Update/View、核心装配app_normal.go(182)— Normal 路由 + Ex/Search/n/N 辅助app_leader.go(126)— Leader 路由 + 动作辅助app_commands.go(234)— Gateway tea.Cmd 工厂与消息类型app_view.go(139)— 视图布局与尺寸适配事件清理:
RunStarted/ 会话切换清 Search/Ex;stream 增长标记Search.Stale。提交历史(11 个规范中文 commit)
破坏性变更
m:toggle Agent mode → 模型选择器(迁移到:mode//mode)c:manual compact → 取消运行(迁移到:compact//compact)g保持单键到顶(未改成 vim 双键gg)。详细手工验证指引
准备
启动 TUI(fake 后端,无需真实 Gateway)
手工验证(按顺序操作,观察底部状态行
[input]/[normal]/[leader])1. Input Mode 编辑(默认
[input])hello worldCtrl+A→ 光标到行首;Ctrl+E→ 光标到行尾Ctrl+W→ 删除 "world"(剩 "hello ")Ctrl+K→ 光标移到中间后按,删除到行尾Ctrl+D(非空)→ 删光标后字符;清空后再按Ctrl+D→ 退出程序(EOF)2. 模式切换
Esc→ 进入[normal];i→ 回[input]3. Normal Mode 导航
i进 Input,输入test回车(让 stream 有内容)Esc进 Normal,j/k滚动g→ 到顶;G→ 到底Ctrl+F→ 整页下翻;Ctrl+B→ 整页上翻Ctrl+D→ 半页下翻;Ctrl+U→ 半页上翻4.
:命令行:→ 底部出现:输入行debug回车 → stream 显示Debug: truehelp回车 → 打开帮助(Esc 关闭)compact回车 →Compact triggeredmode回车 →Agent mode: buildq回车 → 退出bogus回车 →Unknown ex command: bogus5.
/搜索/→ 底部/输入行error(或已有词)回车 → 跳首个匹配n→ 下一个;N→ 上一个(循环)results may be stale — press / to refresh/zzz回车 →No matches: zzz6. Leader Mode
Space→ 进入[leader](Accent 粗体)Space m→ 模型选择器;Space s→ 会话选择器;Space h→ 帮助Space c(运行中)→ 取消;(空闲)→ 静默Space r(有历史)→ 重试;(无)→No previous run to retrySpace Space(有上一会话)→ 切换;(无)→No previous session to switchSpace n→ 新建会话;Space q→ 退出Space x(非后缀)→ 静默回 Normal7. Ctrl+C 双退
Ctrl+C→ 取消运行Ctrl+C→Press Ctrl+C again to quit;2 秒内再按 → 退出