Grok Build 渲染:为什么好看又安静
渲染不是「一个 crate 画一切」。Shell 产出对话事实,Pager 用 Elm 状态机持有 UI 模型,
Markdown/Mermaid 把内容变成可缓存的行,ratatui-inline 做双缓冲 diff,
pager-render 的 draw_frame + 后台 writer 把变更原子写到终端。
「没有一句废话」则是 prompt 约束 × 工具结果预算 × UI 默认折叠 的三层叠加。
一句话心智模型
切面入口
帧循环
biased select! · ACP 节流 · animation demand · idle 零字节
Scrollback
typed blocks · verb-group · sticky · 虚拟滚动
流式 Markdown
checkpoint freeze · OpenCodeHighlighter · pretty transforms
工具卡片
一行动词 · Truncated · turn_status 分离
Composer
TextArea chips · slash/@ · ghost · 发送队列
零废话
output_efficiency · concise tools · UI 折叠
Mock:用户眼里的一帧
设计原则
渲染好看、安静、快——三件事被拆进不同层,互相不越界。
BlockContent 只产 BlockOutput;EntryRenderer 写 cell。可测、可虚拟滚动。
Read 3 files;thinking 收成时长;破坏性操作不跟读操作糊成一团。
output_efficiency:像技术博文一样写,但长度跟任务复杂度成比例——反 filler,不是反语法。
三种 ScreenMode
| 模式 | Viewport | 历史 | 代价 |
|---|---|---|---|
| Fullscreen | Alt-screen 全屏 | 应用内 ScrollbackPane 虚拟滚动 |
完整 theming / 选择 / sticky |
| Inline | 主屏底部 pinned viewport | 应用模型 + 贴底视口 | resize 用 RIS 重放,换一致性 |
| Minimal | 小 live region | 终端原生 scrollback(insert_before 只写一次) |
无完整交互 ScrollbackPane;固定 16 色 |
Crate 地图
渲染相关 crate 的依赖与边界。Shell 不画 UI;像素全在 pager 侧。
职责表
| Crate | 职责 | 关键路径 |
|---|---|---|
| pager | AppView、scrollback、views、ACP tracker | crates/codegen/xai-grok-pager/src/ |
| pager-render | draw_frame、Theme、glyphs、clipboard、能力探测 | crates/codegen/xai-grok-pager-render/src/ |
| markdown | StreamingMarkdownRenderer、syntect、pretty transforms | crates/codegen/xai-grok-markdown/src/ |
| ratatui-inline | 差分 Terminal、insert_before、OSC8 link 层 | crates/codegen/xai-ratatui-inline/ |
| ratatui-textarea | 多行编辑、原子 chip、wrap | crates/codegen/xai-ratatui-textarea/ |
| pager-minimal | commit 历史 + live 区;经 fn-pointer IoC 挂到 pager | crates/codegen/xai-grok-pager-minimal/ |
Minimal IoC 缝
pager 不能依赖 minimal(否则 cargo cycle),minimal 又要读 AppView。
解法:二进制组合根在启动时 minimal::install(),
把 draw fn-pointer 塞进 pager 的 minimal_hook。
帧循环
薄 tokio::select! 只做 IO 管道;状态与渲染委托给 AppView。不是每事件必画。
事件源(biased 优先级)
input_rx 空;batch drain ≤32;键盘随时抢占何时触发 draw
| 源 | 策略 |
|---|---|
| 键鼠(有视觉变化) | 立即 |
| 仅 Resize | 16ms debounce |
| ACP state_changed | min_draw_interval 节流;过快则 schedule 补画 |
| animation_tick | 仅 app.tick()==true(spinner / wave) |
| Idle | 不 tick、不 draw → 光标能真正 blink |
ACP → Scrollback
后台 agent 的 chunk 仍写入自己的 scrollback;只有活动视图受影响才返回 needs_draw。
一帧生命周期
try_draw:它每帧无条件发 cursor 序列,会重置 blink。
Scrollback 块系统
typed block transcript:用户/助手是「开放」块;thinking/tool 是可折叠、可分组的 chrome。
RenderBlock 分类
对话主线
UserPrompt— 色带分隔 · stickyAgentMessage— 流式 MD · 不可 foldThinking— 默认 Truncated → finish Collapsed
工具 / 任务
- Execute · Read · Edit · Search…
SubagentBlock一行 activityBgTaskBlock始终 collapsed
系统 chrome
SessionEvent· System · Btw- ContextInfo · CreditLimit
- Turn status 不在 scrollback 里
水平几何
高度策略
bulk 用 estimate;进入视口才 exact measure;virtual_y: Vec<usize> 支持超长会话。
DisplayMode
Collapsed 一行摘要 · Truncated 头尾预览 · Expanded 全文。
两套 fold(verb 优先)
| GroupKind | 条件 | UI |
|---|---|---|
| VerbRun | 连续只读工具 + finished thoughts(默认 on) | Read 3 files, Searched 2 patterns |
| Truncation | 密排 collapsed 超过 group_max_visible |
╶╶ N more / Ran 6 commands |
绘制路径
流式 Markdown
不是增量 AST——是「未冻结尾巴全量 re-parse + depth-0 checkpoint 冻结 prefix」。
端到端
何时能 freeze
仅 depth==0 块边界:Heading / Paragraph(+blank) / CodeBlock(closed) / List / Table / ThematicBreak…
可 freeze
顶层段落结束、闭合 fence、分割线。冻结后 Vec<Line> 对象稳定,UI 抖动少。
永不 freeze(在容器内)
list / quote / table 内部。整容器留在 tail,每 chunk 重算——靠 closed-fence memo 控 syntect。
语法高亮双路径
| 场景 | 实现 |
|---|---|
| Batch / finish() | HighlightLines 整块 |
| Streaming open fence | OpenCodeHighlighter:按行 commit ParseState,O(N) |
| List 内已闭合 fence | closed_memo(256 KiB body 预算) |
Pretty 语义替换
列表
- → •
引用
> → │
规则
--- → ───
链接
[t](u) → t (u) + OSC8
表格
盒线预排版,按 viewport 缩列宽
Mermaid
内联 Unicode 框;PNG 懒 + 子进程
工具卡片 UX
一行动词卡 + 可选深潜 + 固定状态条。高信息密度,无废话。
三层信息
Scrollback 块
这次工具做了什么(默认可折叠)
Turn status
此刻在干什么(1 行,idle 高度 0)
顶栏 chip
目标 / MCP / token 预算
折叠标题形态
| 工具 | Collapsed | 展开 |
|---|---|---|
| Read | Read main.rs (1-10) | 前 5 + 后 3 行 + 行号 gutter |
| Edit | Edit path +N/-M | 高亮 diff;file-scoped HL 渐进升级 |
| Execute | description 或 $ cmd | Truncated 头尾 stdout;失败红 bullet |
| Search | Search "p" (3 matches…) | 按文件分组匹配行 |
| Web* | query / URL + count | 最多 10 行 + Enter 全屏 |
| MCP Use | Server Action | key:value + 预览;不进 eager verb fold |
Turn status 是「正在发生」的真理源
- Idle → 整行高度 0(消失)
- Tool 有 description → 用
{subject}…,不写 "Waiting for" - watching · N monitors 用更慢脉冲,不抢主 spinner
密度原则
动词优先
动作 · 目标 · 规模(count/diffstat)
默认折叠
展开是特权;全文进 fullscreen
状态 / 结果分离
running 不 dump stdout 进历史墙
失败最小红色
红 bullet / · N failed,不整页红
相同动词聚合
Read 10 files > 10 行 basename
破坏性不对称
Edit/Execute 不进 eager fold
Composer 输入
TextArea 原子编辑核 + PromptWidget 领域叠加 + AgentView 键位编排。
分层
| 层 | 类型 | 职责 |
|---|---|---|
| 编辑核 | TextArea | 缓冲 · wrap · 光标 · undo · 原子 element |
| 领域壳 | PromptWidget | @ / slash / ghost / image;不处理 focus |
| 编排 | AgentView | 键位优先级 · dropdown · 发送 / 队列 |
| 副作用 | dispatch/prompt | 入队 · ACP · slash 执行 |
原子 chip
光标不能落在 chip 内部;删除/选区扩展到边界;undo group 一次撤销整次补全。
为什么「紧」
Placeholder 哲学
默认 "Build anything"——一句意图,不是操作手册。仅 unfocused + empty 显示;聚焦时空输入只留 caret。
Ghost 分层
slash args / shell 补全 / next-prompt suggestion 就地续写;互斥门控,不抢行。
Dropdown 对齐
Fullscreen 锚在 prompt 上方;Minimal 锚下方。Clear + 同 inset,选中行居中滚动。
发送反馈
composer 立刻清空;运行中 → queue 行;空 Enter + 有队列 = send now。Shortcuts bar 状态驱动。
Draw 管线细节
pager-render 的 presentation primitives:与 event loop 解耦的帧写出。
TermWriter 解耦
send(Vec<u8>);慢终端只卡 writer 线程。
挂起 $EDITOR 前 WriterSync::wait_drained,避免帧字节落到子进程 alt-screen。
Renderable 契约
装饰组合:Padded(Accented(BlockRenderer))。生产路径用 EntryRenderer 内联同一布局 + wave/skip_rows。
能力矩阵(fail-closed)
Truecolor
supports-color + 品牌补丁;TokyoNight 等无 truecolor 时从 picker 隐藏
Kitty Keyboard
黑名单 skip;无 KKP 时 UI 推 Alt+Enter
OSC 8 / 图形
超链接进 cell diff;Kitty/iTerm2 图协议;tmux 常禁用图形
主题 · 布局 · 性能
全 RGB 定义 + 启动量化;热路径目标亚毫秒级/帧。
内置主题
| ThemeKind | Truecolor? | tmTheme |
|---|---|---|
| GrokNight(默认) | 否(可量化) | grok-night |
| GrokDay | 否 | grok-day |
| TokyoNight | 是 | tokyo-night |
| RosePineMoon / Oscura | 是 | 共用 night |
| Auto | 运行时 | system_appearance / OSC 11 |
无用户自定义主题文件;可配的是 pager.toml 布局/动画/块样式。
主会话垂直堆叠
缓存边界(bench.md)
| Stage | Cached? | Hot path? |
|---|---|---|
| Markdown parse / syntect | Yes | No |
| Word wrap | Yes (width, gen) | No |
| EntryRenderer + scratch | No | Yes ~80% |
| Buffer diff + flush | N/A | Yes |
为什么没有一句废话
不是靠单一 “be concise”。是 教模型怎么写 × 给模型精简工具反馈 × 给用户默认折叠的 TUI 三层叠加。用户感知的安静,是过程信息从未以散文进入视野中心。
四层反 fluff 栈
Prompt 原文(默认路径)
crates/codegen/xai-grok-agent/templates/prompt.md · <output_efficiency>
templates/apply_patch_prompt.md · TemplateOverride::Codex
通道隔离
NEVER use bash echo 与用户沟通。Output all communication in response text。砍掉 echo "I'm about to…" 污染。
辅助模型也有字数合同
Recap = 1 句 25–40 词;Goal summarizer ≤80 words / 4 bullets;Session title 5–10 词 no filler;/btw 禁止 “Let me try…”。
模型侧 vs 用户侧:双通道
模型通道(可较长)
tool stdout、content、system-reminder、compaction summary——推理需要足够细节。
用户眼睛(默认短)
ToolCallBlock header · verb-group · turn_status · 折叠 thinking。不是把 output_for_prompt 贴进聊天。
UI 默认不展示过程细节
| 机制 | 默认 | 效果 |
|---|---|---|
group_tool_verbs | true | 连续 read/search → 一行聚合 |
| Thinking display | Truncated → Collapsed | 推理不占屏 |
| Tool DisplayMode | Collapsed | 成功路径默认 1 行 |
| todo_write | 3+ steps | 进度走 pane 不是聊天日记 |
| Tips | 1 行 + seen cap | 不堆 toast |
Headless -p | RUST_LOG off | 只打印 answer |
与典型 Agent UI 对比
典型 coding agent
每步 “I'll now open X…” 长叙事
工具 args/result 大段贴 markdown
CoT 默认展开
成功后复述 diff/plan 全文
用 shell echo 报告进度
进度 = 聊天日记
Grok Build
preamble 很短;trivial read 可无
折叠 header + verb 聚合
Thought for Xs 折叠
plan/diff harness 展示;prompt 禁复述
明确禁止 echo 聊天
进度 = todo + turn_status + verb-group
关键源码索引
| 层 | 路径 |
|---|---|
| 默认 system | xai-grok-agent/templates/prompt.md |
| Codex 文风 | xai-grok-agent/templates/apply_patch_prompt.md |
| Concise tools | xai-grok-tools/.../grok_build_concise/ |
| use_concise | xai-grok-shell/src/agent/config.rs |
| verb-group | xai-grok-pager/src/scrollback/state/verb_group.rs |
| turn_status | xai-grok-pager/src/views/turn_status.rs |
| draw_frame | xai-grok-pager-render/src/render/draw.rs |
| streaming MD | xai-grok-markdown/src/streaming.rs |
| event loop | xai-grok-pager/src/app/event_loop.rs |
| perf 架构文 | xai-grok-pager/benches/bench.md |