Skip to content

docs(v2): C28 Keybindings、Vim 模式与 Voice 输入 (YAO-127) - #65

Merged
luyao618 merged 5 commits into
mainfrom
agent/cc-dev/yao-127-c28
May 27, 2026
Merged

docs(v2): C28 Keybindings、Vim 模式与 Voice 输入 (YAO-127)#65
luyao618 merged 5 commits into
mainfrom
agent/cc-dev/yao-127-c28

Conversation

@luyao618

Copy link
Copy Markdown
Owner

摘要

新增 docs/31-Keybindings-Vim-与-Voice-输入.md(C28),按 V2-REVISION-SPEC §0.5.4 新章规则写成叙事博客而非参考手册。

三段结构:

  • Keybindings:18 上下文、默认绑定表的跨平台分支、Ink 的 alt/meta 归一化与 Escape 假 meta 补丁、chord 解析的 chordWinners 仲裁、useKeybinding 的 false 协议与 stopImmediatePropagation、用户自定义被 tengu_keybinding_customization_release 守、reservedShortcuts 三档不可绑定键。
  • VimVimState INSERT/NORMAL 双外层 + CommandState 11 变体内层状态机、3dw / di( / df, 三类典型流的状态转移、MAX_VIM_COUNT = 10000 截断、RecordedChange union 支撑 . dot-repeat、Escape 故意不走 Keybindings 而是写死在 useVimInput.ts:189-195 的设计原因。
  • Voicetengu_amber_quartz_disabled 反向 kill switch + 鉴权双闸、cpal → arecord → SoX rec 三档录音回落(含 linuxHasAlsaCards 早返与 150ms probeArecord race)、WebSocket 心跳 8s + finalize 双计时器(noData 1500 / safety 5000)、FinalizeSource union 四档分桶、hooks/useVoice.ts 四常数(RELEASE 200 / FIRST_PRESS_FALLBACK 2000 / REPEAT_FALLBACK 600 / FOCUS_SILENCE 5000)压"按住空格"交互、20 BCP-47 语言归一化到 en 兜底。

本地 CI 闸结果

结果
C-3 代码块占比 1.2% ≤ 25% ✅
C-4 小标题禁词 26/26 标题合规 ✅
C-5 无 frontmatter 通过 ✅
lint-no-fuzzy-quantifiers 无禁词 ✅
lint-no-spec-jargon 无 squad 术语 ✅
lint-no-revision-codenames 39 文件无泄漏 ✅
check-source-commits 6 文件全指向 290fdc94 ✅
gen-module-matrix --check-orphans covered=31 ✅

Manifest diff 摘要

无 manifest 变更:本章只新增 docs/31-Keybindings-Vim-与-Voice-输入.md 一个文件;scripts/check-code-ratio.tsNEW_CHAPTER_FILES 由 YAO-140 预先登记,本 PR 不动 scripts/。


风格双亲实证

风格双亲:v1-03 状态管理 — React 与非 React 世界的状态桥接 + v1-21 Ink 框架深度定制 — 在终端中运行 React

v1 原文摘抄(≥ 200 字 × 2 段)

【摘抄 1,来自 docs/03-状态管理.md "为什么状态管理值得单独一篇?"】

Claude Code 面临一个独特的状态管理难题:它既是一个 React 应用,又不完全是

终端 UI 用 Ink(React for CLI)渲染,组件需要响应式的状态更新。但核心业务逻辑 —— API 调用、工具执行、Agent 编排 —— 运行在 React 树之外。一次工具调用的结果需要同时:

  1. 更新 React 组件(显示在终端 UI 上)
  2. 被非 React 的 query.ts 对话循环读取
  3. 被 Agent 子系统使用(可能运行在隔离的上下文中)

如果用 Redux/Zustand 这类库?太重了。React 内置的 useState/useReducer?无法从 React 树外部访问。模块级全局变量?无法触发 React 重渲染。

Claude Code 的答案是:三层状态架构 + 一个 35 行的自研 Store

【摘抄 2,来自 docs/21-Ink框架深度定制.md "为什么要 Fork Ink?"】

Ink 是一个开源框架,让你在终端中使用 React 组件编写 UI。官方 Ink 适合简单的 CLI 工具 —— 但 Claude Code 不是简单的 CLI。它需要:

  • 全屏模式:Alt Screen 下的完整 UI,不是"追加式"输出
  • 虚拟滚动:对话历史可能有上千行,不能全部渲染
  • 鼠标交互:点击、拖拽选择文本、滚轮滚动
  • 60fps 渲染:流式输出时每 16ms 刷新一帧,不能闪烁
  • IME 支持:CJK 输入法需要物理光标精确定位

官方 Ink 不支持这些。Claude Code 团队 fork 了 Ink 并进行了大量深度定制,最终形成了一个功能完备的终端 React 渲染引擎。

本章新写正文摘抄(≥ 200 字 × 2 段,覆盖典型叙事段)

【新写 1,来自 docs/31-Keybindings-Vim-与-Voice-输入.md "为什么要把这三块放在同一篇里讲?"】

当你打开终端,敲下 claude,REPL 起来之后你按下任意一个键,会发生什么?这背后其实不只一条路。Ink 把原始按键事件喂给上层;上层要决定的,是把这个按键当成"用户输入的字符"喂进编辑框,还是当成"快捷键"派给某个动作,还是当成"我此刻按住空格在录音"喂给麦克风。三种解读模式共用同一条 Ink 输入流,又各自维护一套自己的状态——这就是本章想要拆开看的"输入层"。

放在书脊上看,C26 讲了 Ink 的渲染与组件系统,C27 讲了多模态文件上传,C28 这一篇要补的是介于"原始按键"与"高层意图"之间的那一层胶水:默认快捷键怎么注册、用户怎么覆盖、Vim 模式怎么用一套状态机把单字符串成命令、Voice 又怎么把"按住一个键"翻译成一段 16 kHz 的 PCM 流并推给后端做 STT。三套子系统目录是分开的(keybindings/vim/voice/ + services/voice* + hooks/useVoice*),但它们的共同点是:都坐在 Ink 的 useInput 之上,又都要绕开 Ink 默认的"按一下出一个字符"的语义。

【新写 2,来自 docs/31-Keybindings-Vim-与-Voice-输入.md "为什么 Vim 这一段值得单独讲?"】

终端里的输入框做 Vim 模式,有两种典型做法。一种是 textarea 加一组 keymap,按下哪个键执行哪段代码;这种做法在做到 i / a / x 这种单字符命令时还行,做到 3dw / gg / cit 这种带计数、带 motion、带 text object 的复合命令时就会变成一团乱麻。另一种做法是显式建一个状态机,把"我按到一半"这件事变成一个一等的"中间状态",让每一次按键都是一次状态转移。

Claude Code 走的是后一条路。vim/types.ts 顶部的 CommandState 是一个 11 个变体的 discriminated union(types.ts:59-75):idlecountoperatoroperatorCountoperatorFindoperatorTextObjfindgoperatorGreplaceindent。读者读到这里可以先把这 11 个名字记下来,下面会把每一个名字在什么时候出现讲清楚。整套 Vim 子系统挂在外层一个更简单的状态变量上:VimState 只有 INSERTNORMAL 两个值(types.ts:49-51)。INSERT 里没有任何状态机,就是普通的文本输入;NORMAL 里挂着上面那个 11 变体的子状态机。模式之间靠 Escapei / a / o 等键切换。


Closes YAO-127。不要 merge——尧哥手动合

Yao Lu and others added 5 commits May 27, 2026 12:13
新增 docs/31-Keybindings-Vim-与-Voice-输入.md,按 V2-REVISION-SPEC §0.5.4
新章规则写成叙事博客而非参考手册:先讲为什么三个输入子系统值得放在一起,
然后分三段——Keybindings(18 上下文 / 默认表 / Ink 修饰键归一化 / chord 解析
/ 用户覆盖闸)、Vim(11 状态变体的 DAG / count 上限 / dot-repeat / Escape
有意不走 Keybindings 的设计)、Voice(双闸 / 三档录音回落 / WebSocket 心跳
与 finalize / 按住空格的时序状态机 / 20 语言归一化)。

CI 闸(本地预跑全绿):
- C-3 代码块占比 1.2% ≤ 25%
- C-4 26 个标题全部合规
- C-5 无 frontmatter
- lint-no-fuzzy-quantifiers 无禁词
- lint-no-spec-jargon 无 squad 内部术语
- check-source-commits 全章指向 290fdc94
- gen-module-matrix --check-orphans 通过

风格双亲:v1-03 状态管理 + v1-21 Ink 框架深度定制(详见 PR 描述)。

Co-authored-by: multica-agent <github@multica.ai>
- 上下文清单与源码 schema.ts:12-32 对齐为 18 个
- 动作 ID 改为 app:exit / voice:pushToTalk 等
- MODE_CYCLE_KEY 改为 VT-模式判定(非 macOS 区分)
- resolver / false 协议 / invokeAction 按源码重写
- dot-repeat 修正 INSERT 路径
- FinalizeSource 改为 5 分支
- 语言列表改为 20 个 base code
- 增补附:源码引用清单

Co-authored-by: multica-agent <github@multica.ai>
- L97 重写:举例改为合法 context('Chat' / 'Autocomplete' / 'Global'),
  明确 uniqueContexts 只用于构 Set 过滤 bindings,不是按数组顺序找第一个
  匹配;胜负仍由 bindings 数组的 last-wins 决定,对齐 resolver.ts:192-228。
- 删除 ## 附:本章源码引用清单 整节:V2-REVISION-SPEC.md:231 + §0.5.5
  明确禁止此类脚手架外漏到正文,CI lint-no-spec-jargon-in-prose 也会 fail。
- 把原附录里的 commands/{vim,voice,keybindings}/ 与 hooks/useVoiceEnabled.ts
  以叙事方式补回正文,确保 required anchors 仍在正文出现。

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
@luyao618
luyao618 merged commit 30190cc into main May 27, 2026
1 check passed
@luyao618
luyao618 deleted the agent/cc-dev/yao-127-c28 branch June 3, 2026 08:05
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.

1 participant