- 人工智能
- 大模型
- AI Agent
- 代码智能体
- CLI
- 工具调用
- MCP Clients
【免费下载链接】grok-build
SpaceXAI's coding agent harness and TUI. Fullscreen, mouse interactive, extensible.
导读
本篇文章基于 grok-build 仓库中 0.2.91 版本变更日志,深入剖析该版本聚焦的两项终端 UI 修复:其一,在 plan mode(计划模式)审查阶段,语音听写指示器与[stop]停止按钮仍保持可见、可点击;其二,"New Worktree" 新建工作树对话框在窄终端下会自适应扩张以展示长名称,并在空间不足时以前导省略号滚动。文章将以仓库源码(xai-grok-pager TUI 渲染层与输入分发层)为佐证,说明这两处修复的底层机制、测试保障与可复现路径,帮助你在阅读或调试 grok-build 的终端交互时快速定位对应实现。
版本背景与变更范围
0.2.91(发布于 2026-07-07)是xai-grok-shell(grok-build 的编码代理终端外壳)迭代版本之一。当前仓库中该 crate 的 Cargo.toml 版本号已演进至1.0.41,但历史变更记录仍完整保留在changelogs/目录下,每条记录同时提供 Markdown(0.2.91.md)与 JSON(0.2.91.json)两种格式,便于脚本与人工阅读。两条变更的 JSON 元数据均为"category": "fixes"、"breaking_change": false,即纯缺陷修复、无破坏性变更:
| 变更项 | 类型 | 影响面 |
|---|---|---|
| 语音听写指示器与停止按钮在 plan mode 审查期间保持可见、可点击 | fix | 输入分发、鼠标命中区域、渲染层级 |
New Worktree 对话框在窄终端下扩张显示长名称并以前导…滚动 | fix | 欢迎页对话框渲染、单行文本编辑器的视口逻辑 |
下文分别从「问题场景 → 实现机制 → 测试佐证」三个层面展开。
修复一:plan mode 审查阶段语音听写指示器的可用性
问题场景
grok-build 的 TUI 支持语音听写(voice dictation):按下Ctrl+Space(或/voice)开启录音后,界面底部会出现一条录制指示行,包含脉冲红点、Recording字样与右侧的[stop]按钮。该按钮是一个可点击的鼠标热区,用于随时终止录音。
问题出在 plan mode(计划模式)的审查环节:当 Agent 提交计划、进入 plan approval 视图时,行级查看器(line viewer)会作为覆盖层接管鼠标路由;同时计划反馈(plan feedback)阶段提示框聚焦在 prompt 面板。在这两种状态下,用户仍希望继续/终止听写,但若指示行被覆盖层遮挡、或点击事件被查看器吞掉,录音便无法通过 UI 停止。
实现机制:三层保障
第一层是渲染可见性。录制指示行由 render.rs 负责绘制:当voice_listening为真且布局中voice_recording区域有效时,依次绘制脉冲红点、Recording文案与右侧的[stop]按钮,并在渲染结束时把按钮区域写入self.hit_voice_stop_button.rect,作为后续鼠标命中的依据:
let stop_str = "[stop]"; let stop_w = unicode_width::UnicodeWidthStr::width(stop_str) as u16; let stop_x = rec_area.x + rec_area .width .saturating_sub(layout_cfg.block_pad_right + stop_w); // ...绘制 [stop],并根据 hovered 状态切换前景色(accent_error / gray) self.hit_voice_stop_button.rect = Some(Rect::new(stop_x, rec_area.y, stop_w, 1));关键点在于:plan approval 的 line viewer 覆盖层在布局时显式排除了录制指示行,因此该行在审查阶段始终可见,而不是被覆盖。
第二层是鼠标路由。plan approval 的行级查看器虽然持有鼠标路由,但[stop]的点击需要穿透覆盖层、直接命中VoiceToggle动作。测试注释明确写道:"Recording-row [stop] click keeps working while the plan approval's line-viewer overlay owns mouse routing. The row stays visible (the overlay excludes it), so the viewer must not swallow the click."(input.rs)。这意味着输入分发逻辑在覆盖层命中判定前,会优先检查hit_voice_stop_button.rect命中的鼠标坐标,将其映射为Action::VoiceToggle。
第三层是动作语义。dispatch/voice.rs 中的dispatch_voice_toggle是[stop]按钮、Ctrl+Space、录音中按 Esc 等入口的统一汇聚点:
pub(super) fn dispatch_voice_toggle(app: &mut AppView) -> Vec<Effect> { if app.voice_listening() { // Stop always succeeds, even if the remote flag or `/voice` mode flipped mid-recording app.voice_stop_keeping_final(); return vec![]; } // 未录音则启动:与 /voice 一致,一次按键即拉起横幅(banner) dispatch_enable_voice_mode(app, /* from_hold */ false) }注释特别强调:停止操作总是成功,即使远端 flag 或/voice模式在录音中途被翻转——这与 0.2.91 修复"审查阶段停止按钮始终可点击"的目标完全吻合。此外,dispatch_voice_toggle在 plan feedback 阶段同样生效,因为此时提示框聚焦于 prompt 面板,按钮热区仍参与常规命中检测。
测试佐证
修复随附的测试位于 input.rs 的voice_stop_click_during_plan_review_tests模块,覆盖两个场景:
stop_click_dispatches_voice_toggle_under_plan_approval_viewer:打开 plan approval 并确认 line viewer 已接管后,模拟在(91, 30)坐标点击[stop],断言分发结果为Action::VoiceToggle——证明覆盖层不吞掉该点击;stop_click_dispatches_voice_toggle_in_plan_feedback:关闭 viewer、prompt 面板聚焦时点击[stop],同样断言VoiceToggle——证明反馈阶段的点击也不丢失。
两个测试均通过test_fixtures::{make_agent, make_plan_approval_view_state}构造状态,直接验证"plan mode 审查全程可停止语音"的用户可见行为。
修复二:New Worktree 对话框的自适应宽度与滚动
问题场景
在欢迎页(welcome screen)通过New Worktree弹窗创建工作树时,用户可以输入可选的名称标签(label),留空则自动生成名称。旧实现中对话框宽度固定,遇到较长名称时要么被截断、要么溢出终端,且窄终端下无法看到正在输入的内容末尾(光标一侧)。
实现机制:宽度自适应 + 前导省略号滚动
对话框渲染完全位于 new_worktree_dialog.rs,其核心是两个常量与一个宽度函数:
/// Minimum dialog width (fits the title, an empty input, and the hints). const MIN_DIALOG_WIDTH: u16 = 50; const DIALOG_HEIGHT: u16 = 5; const INNER_PAD: u16 = 4; // 边框内侧左右内边距(inner_x = dialog.x + 2) const LABEL_PREFIX: &str = "Name (optional): ";宽度自适应由dialog_width_for完成(new_worktree_dialog.rs):
fn dialog_width_for(area_width: u16, label: &str) -> u16 { let max_width = area_width.saturating_sub(4); // The extra 1 is the block cursor cell let needed = (LABEL_PREFIX.width() + label.width() + 1 + INNER_PAD as usize) as u16; needed.max(MIN_DIALOG_WIDTH).min(max_width) }- 对话框宽度 =
max(需求宽度, 50),再钳制到终端宽度 - 4的上限; - 需求宽度包含前缀
"Name (optional): "、已输入标签的显示宽度、1 个块状光标列与内边距; - 因此名称越长对话框越宽,直到占满终端可用宽度为止,标题与提示行始终容纳。
前导省略号滚动由输入状态机的视口逻辑承担。对话框的输入标签使用LineEditor存储,状态定义在 app_view.rs,并通过xai_ratatui_textarea::SingleLineViewport计算可见字节区间与光标显示列:
pub(crate) fn viewport(&self, width: usize) -> xai_ratatui_textarea::SingleLineViewport { self.label.viewport(width) }渲染时(new_worktree_dialog.rs)只取视口中的可见片段,并据cursor_display_column在对应单元格绘制块状光标:
let viewport = state.viewport(input_width as usize); let visible_input = state.label().get(viewport.visible_byte_range).unwrap_or(""); // 绘制 "Name (optional): " + visible_input if input_width > 0 { let cursor_x = inner_x + prefix_w + viewport.cursor_display_column as u16; // 在该单元格设置 block cursor 样式 }当终端宽度不足以完整显示标签时,视口保持光标侧(末尾)可见并滚动起始位置,被滚出视野的开头以…指示——这正是 changelog 中"scrolls with a leading …"的含义。另外,标签文本输入受MAX_WORKTREE_LABEL_BYTES = 100字节上限约束(app_view.rs),粘贴与键入均受此限。
测试佐证
new_worktree_dialog.rs 内置了覆盖五种情况的测试:
| 测试 | 断言 |
|---|---|
empty_dialog_uses_minimum_width | 空标签时宽度恰为MIN_DIALOG_WIDTH(宽终端)或面积-4(窄终端) |
dialog_grows_with_long_label | 长标签使对话框宽度超过最小值,且内部空间足够容纳完整标签与 UI 元素 |
dialog_clamps_to_terminal_width | 100 字符标签在 60 列终端下被钳制到 56 列,不越界 |
long_name_fully_visible_on_wide_terminal | 宽终端(100 列)下完整长名称可见,标题New Worktree也在 |
long_name_end_visible_on_narrow_terminal | 窄终端(40 列)下标签末尾(光标侧)保持可见,且画面含…或尾部文本 |
narrow_dialog_keeps_middle_unicode_cursor_visible | 标签含 CJK、组合音标与 emoji ZWJ 序列(如👩🏽💻)时,光标单元格在窄对话框内仍然可见 |
其中narrow_dialog_keeps_middle_unicode_cursor_visible尤其值得注意:它构造了xxxxxxxxxxxx中é{👩🏽💻}tail这样包含多字节与宽字符的标签,并验证光标命中单元格——说明视口逻辑正确处理了**显示宽度(column)与字节偏移(byte range)**的换算,emoji 组合序列不会被拆散导致光标漂移。
两条修复背后的通用设计
从源码结构看,这两条修复体现了 xai-grok-pager 终端交互层的两个通用设计原则,可作为阅读该 crate 的索引:
- 命中热区与渲染同源。
[stop]按钮的命中矩形(hit_voice_stop_button.rect)在渲染函数中随布局一并计算并存储(render.rs),输入层直接复用该矩形做命中判定,避免"渲染与点击区域不一致"这类经典 TUI 缺陷;同目录下 links.rs 的遮蔽(occluder)测试进一步证明,命中判定还考虑了覆盖层遮挡顺序。 - 覆盖层与宿主行互斥。plan approval 的 line viewer 覆盖层在布局时排除录制指示行,使指示器天然保留在交互层之上;这一约定通过
voice_stop_click_during_plan_review_tests固化,防止未来新增覆盖层时回归。
如何验证与复现
仓库是只读镜像,你可以通过以下方式自行验证这两处行为:
- 阅读变更记录:对照 0.2.91.md 与 0.2.91.json,或查看 CHANGELOG.md 中的汇总条目;
- 运行渲染层单测:在 xai-grok-pager 目录下执行
cargo test -p xai-grok-pager views::new_worktree_dialog与cargo test -p xai-grok-pager voice_stop_click_during_plan_review,可分别跑通两个修复对应的测试模块(具体测试名以cargo test -p xai-grok-pager -- --list输出为准); - 实际体验:在较窄的终端(约 40 列)中打开欢迎页的 New Worktree 对话框输入长名称,观察对话框变宽与
…前导滚动;在开启语音听写(Ctrl+Space)后进入 plan mode 审查,确认[stop]仍可点击并正常结束录音。
小结
0.2.91 的两条变更虽小,却分别触及 TUI 交互的两个关键面:语音听写指示器修复保证了"覆盖层出现时关键控件不丢失"的可用性底线;Worktree 对话框则完善了"任意终端宽度下输入长文本不截断"的健壮性。对希望深入 grok-build 终端实现(xai-grok-pager 与 xai-grok-shell)的开发者而言,这两处修复连同其测试是理解鼠标热区路由与单行文本视口机制的极佳起点。
- 人工智能
- 大模型
- AI Agent
- 代码智能体
- CLI
- 工具调用
- MCP Clients
【免费下载链接】grok-build
SpaceXAI's coding agent harness and TUI. Fullscreen, mouse interactive, extensible.
相关推荐
VS Code 语音支持实战指南:Voice Mode 语音对话与内置听写(Dictation)配置详解
VS Code 语音支持实战指南:Voice Mode 语音对话与内置听写(Dictation)配置详解 本文围绕 VS Code(vscode docs 仓库
文档教程grok-build 0.2.80 版本深度解析:会话级命令超时、语音听写与压缩持久化实战指南
grok build 0.2.80 版本深度解析:会话级命令超时、语音听写与压缩持久化实战指南 版本发布说明: crates/codegen/xai grok
人工智能大模型AI Agent代码智能体CLI工具调用MCP ClientsGrok Voice 插件实战:在 Cursor 中为应用接入 Grok 实时语音、听写与朗读,并用日志驱动调试修复
Grok Voice 插件实战:在 Cursor 中为应用接入 Grok 实时语音、听写与朗读,并用日志驱动调试修复 本插件( grok voice )是 Cu
AI 技能AI 插件插件系统AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考