Overview

Grok Build 渲染:为什么好看又安静

渲染不是「一个 crate 画一切」。Shell 产出对话事实,Pager 用 Elm 状态机持有 UI 模型, Markdown/Mermaid 把内容变成可缓存的行,ratatui-inline 做双缓冲 diff, pager-render 的 draw_frame + 后台 writer 把变更原子写到终端。 「没有一句废话」则是 prompt 约束 × 工具结果预算 × UI 默认折叠 的三层叠加。

L0 · 组合根 binary
xai-grok-pager-bin
main · install minimal · headless
L1 · 应用编排 Elm: Action → Effect
event_loop
select! · throttle · ticks
AppView / AgentView
state · layout · draw
dispatch / effects
sync mutate · async IO
L2 · 内容模型 typed transcript
ScrollbackState
entries · groups · layout
RenderBlock
user · agent · tool · think
MarkdownContent
streaming · wrap cache
L3 · 呈现原语 pager-render
draw_frame
sync output · cursor
Theme / glyphs
quantize · tmTheme
TerminalContext
KKP · OSC8 · Kitty
L4 · 终端引擎 ratatui-inline
双缓冲 diff
flush_with_links → bool
Inline viewport
insert_before · RIS
TermWriter
后台线程 · 0-byte idle
Host / Binary App 状态 Transcript Presentation Terminal I/O

一句话心智模型

Shell 产出对话事实;Pager 用 Elm 持有 UI;Markdown 冻结前缀 + 增量 wrap; ratatui-inline 差分写 cell;draw_frame 保护光标 blink 与 PTY 背压。 用户感觉「没有废话」,是因为过程信息大多从未以散文进入视野中心——它们是 verb 聚合、turn status、todo pane。

Mock:用户眼里的一帧

┌ status ────────────────────────────────────────────────────┐ [plan] · 12.3k/50k tokens · 3m └────────────────────────────────────────────────────────────┘ 拆分 grok build 渲染逻辑,做成可视化 Read 4 files, Searched 2 patterns Edit docs/render/index.html +240/-0 Thought for 1.2s Agent 正文是 pretty markdown:标题、代码高亮、表格…… 没有「I'll now open…」墙文——过程在工具行与状态条。 Run cargo test 0.2s 1m20s ⇣12k [stop] ╭──────────────────────────────────────────────────────────╮ _ ╰─ model · yolo ────────────────────────────────────────╯
Principles

设计原则

渲染好看、安静、快——三件事被拆进不同层,互相不越界。

1. 内容 vs 绘制分离 BlockContent 只产 BlockOutputEntryRenderer 写 cell。可测、可虚拟滚动。
2. 前缀永不重算 Markdown checkpoint 冻结已完成块;wrap 按 generation 缓存;滚动只 paint 视口。
3. Idle = 零字节 无 cell 变化且光标不变 → discard 帧缓冲;保护光标 blink,省电省 PTY。
4. 状态与结果分离 「正在做什么」在 turn_status 一行;结果在可折叠工具卡。禁止进度日志洪水。
5. 默认折叠,展开是特权 verb-group 合成 Read 3 files;thinking 收成时长;破坏性操作不跟读操作糊成一团。
6. Harness 已展示则勿复述 plan / diff / todo 由 UI 渲染;模型 prompt 明确禁止再贴一遍全文。
7. Elm 状态机 input → Action → dispatch(sync)→ Effect(async)→ TaskResult。无 IO 的 dispatch 可测。
8. 能力探测 fail-closed 未知终端跳过 KKP/图形;truecolor 主题在低能力终端隐藏。启动安全 > 乐观启用。
9. 比例原则 output_efficiency:像技术博文一样写,但长度跟任务复杂度成比例——反 filler,不是反语法。

三种 ScreenMode

模式Viewport历史代价
Fullscreen Alt-screen 全屏 应用内 ScrollbackPane 虚拟滚动 完整 theming / 选择 / sticky
Inline 主屏底部 pinned viewport 应用模型 + 贴底视口 resize 用 RIS 重放,换一致性
Minimal 小 live region 终端原生 scrollbackinsert_before 只写一次) 无完整交互 ScrollbackPane;固定 16 色
Crates

Crate 地图

渲染相关 crate 的依赖与边界。Shell 不画 UI;像素全在 pager 侧。

xai-grok-pager-bin ├── xai-grok-pager # 产品状态 / views / scrollback │ ├── xai-grok-pager-render # theme · draw · terminal probe │ ├── xai-grok-markdown # streaming MD → Line/Span │ ├── xai-grok-mermaid # lazy PNG (out-of-process) │ ├── xai-ratatui-inline # double buffer · inline viewport │ ├── xai-ratatui-textarea # composer editor │ └── xai-tty-utils # detach children from TTY ├── xai-grok-pager-minimal # native scrollback path (IoC seam) └── xai-grok-shell # agent / ACP server — NOT UI

职责表

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

Facet · Frame

帧循环

tokio::select! 只做 IO 管道;状态与渲染委托给 AppView。不是每事件必画。

事件源(biased 优先级)

01
cancel / quit最高优先级,避免被 ACP 淹没
02
ACP仅当 input_rx 空;batch drain ≤32;键盘随时抢占
03
TaskResult / progresseffects 回流 → 再 dispatch
04
键盘 / 鼠标独立 reader 线程 poll → mpsc;paste coalesce
05
resize debounce · deferred draw · scroll · animation软件 cadence,非 OS vsync
06
voice(最后)~5–20Hz,故意最低

何时触发 draw

策略
键鼠(有视觉变化)立即
仅 Resize16ms debounce
ACP state_changedmin_draw_interval 节流;过快则 schedule 补画
animation_tickapp.tick()==true(spinner / wave)
Idle不 tick、不 draw → 光标能真正 blink
GROK_MIN_DRAW_MS display_refresh probe TickDemand::{None,Slow,Fast}

ACP → Scrollback

shell (xai-grok-shell) ──ACP──► acp_handler::handle │ ▼ AcpUpdateTracker │ ├─ AgentMessageChunk → AgentMessageBlock::push_chunk ├─ AgentThoughtChunk → ThinkingBlock ├─ ToolCall / Update → ToolCallBlock::* └─ UserMessageChunk → UserPromptBlock

后台 agent 的 chunk 仍写入自己的 scrollback;只有活动视图受影响才返回 needs_draw。

一帧生命周期

A
BeginSynchronizedUpdateDCS 同步输出,防 tmux/zellij 撕裂
B
autoresize + get_frame业务 paint Buffer + 收集 OSC8 LinkSpans
C
flush_with_links()cell diff → has_changes: bool
D
swap_buffers若无变化且 cursor 不变 → discard → 0 bytes
E
CursorState.apply去重 Show/Hide/MoveTo,保护 blink
F
EndSynchronizedUpdate + TermWritermpsc → term-writer 线程 → PTY
绕开 ratatui 的 try_draw:它每帧无条件发 cursor 序列,会重置 blink。
Facet · Scrollback

Scrollback 块系统

typed block transcript:用户/助手是「开放」块;thinking/tool 是可折叠、可分组的 chrome。

RenderBlock 分类

对话主线

  • UserPrompt — 色带分隔 · sticky
  • AgentMessage — 流式 MD · 不可 fold
  • Thinking — 默认 Truncated → finish Collapsed

工具 / 任务

  • Execute · Read · Edit · Search…
  • SubagentBlock 一行 activity
  • BgTaskBlock 始终 collapsed

系统 chrome

  • SessionEvent · System · Btw
  • ContextInfo · CreditLimit
  • Turn status 不在 scrollback 里

水平几何

│ A │ PL │ Content (flex) │ PR │ 1 2 Min(1) 1 A = accent ┃(collapsed groupable 用 dim ❙) PL/PR = block padding(compact 时压缩) 选择框画进 outer viewport pad,不占 content 列

高度策略

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
Execute / Edit 不进 eager verb fold——破坏性操作必须单独露脸。

绘制路径

1
prepare_layout(width, height)dirty heights · groups · sticky descriptors
2
render_scrolled_entries只 walk 视口窗口;skip_rows 处理半可见
3
EntryRendererAccented + Padded + BlockOutput → Buffer
4
post-passSelectionBox · OSC8 · search invert · media
Facet · Markdown

流式 Markdown

不是增量 AST——是「未冻结尾巴全量 re-parse + depth-0 checkpoint 冻结 prefix」。

端到端

token chunk → MarkdownContent::push_chunk → StreamingMarkdownRenderer::push_and_render ① LatexDelimiterNormalizer ② truncate to FrozenState ③ parse+render tail only (pulldown-cmark) ④ append; maybe advance freeze → ensure_wrapped (增量 word-wrap) → BlockOutput → scrollback + OSC8

何时能 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。

无 checkpoint:每 chunk O(N) → 总 O(N²)。有 checkpoint:摊销后约 O(N)

语法高亮双路径

场景实现
Batch / finish()HighlightLines 整块
Streaming open fenceOpenCodeHighlighter:按行 commit ParseState,O(N)
List 内已闭合 fenceclosed_memo(256 KiB body 预算)
syntect + two-face grok-night.tmTheme tokyo-night.tmTheme

Pretty 语义替换

列表

-

引用

>

规则

---───

链接

[t](u)t (u) + OSC8

表格

盒线预排版,按 viewport 缩列宽

Mermaid

内联 Unicode 框;PNG 懒 + 子进程

Facet · Tools

工具卡片 UX

一行动词卡 + 可选深潜 + 固定状态条。高信息密度,无废话。

三层信息

Scrollback 块

这次工具做了什么(默认可折叠)

Turn status

此刻在干什么(1 行,idle 高度 0)

顶栏 chip

目标 / MCP / token 预算

Read 3 files, Searched 2 patterns Edit src/lib.rs +12/-3 Run $ cargo test Run cargo test 0.2s 1m20s ⇣12k [stop]

折叠标题形态

工具Collapsed展开
ReadRead main.rs (1-10)前 5 + 后 3 行 + 行号 gutter
EditEdit path +N/-M高亮 diff;file-scoped HL 渐进升级
Executedescription 或 $ cmdTruncated 头尾 stdout;失败红 bullet
SearchSearch "p" (3 matches…)按文件分组匹配行
Web*query / URL + count最多 10 行 + Enter 全屏
MCP UseServer Actionkey:value + 预览;不进 eager verb fold

Turn status 是「正在发生」的真理源

⠧ Run command 0.2s 1m20s ⇣12k [stop] │ spinner + activity(≤40 chars)│ timers + tokens + actions
  • 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

Facet · Composer

Composer 输入

TextArea 原子编辑核 + PromptWidget 领域叠加 + AgentView 键位编排。

分层

类型职责
编辑核TextArea缓冲 · wrap · 光标 · undo · 原子 element
领域壳PromptWidget@ / slash / ghost / image;不处理 focus
编排AgentView键位优先级 · dropdown · 发送 / 队列
副作用dispatch/prompt入队 · ACP · slash 执行

原子 chip

KIND_PASTE · [Pasted: N lines] KIND_FILE_REF · @path KIND_IMAGE · [Image #N]

光标不能落在 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 状态驱动。

Facet · Pipeline

Draw 管线细节

pager-render 的 presentation primitives:与 event loop 解耦的帧写出。

TermWriter 解耦

事件循环只 send(Vec<u8>);慢终端只卡 writer 线程。 挂起 $EDITOR 前 WriterSync::wait_drained,避免帧字节落到子进程 alt-screen。

Renderable 契约

trait Renderable { fn render(&self, area: Rect, buf: &mut Buffer); fn desired_height(&self, width: u16) -> u16; // 必须廉价 }

装饰组合: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 常禁用图形

Facet · Theme & Perf

主题 · 布局 · 性能

全 RGB 定义 + 启动量化;热路径目标亚毫秒级/帧。

内置主题

ThemeKindTruecolor?tmTheme
GrokNight(默认)否(可量化)grok-night
GrokDaygrok-day
TokyoNighttokyo-night
RosePineMoon / Oscura共用 night
Auto运行时system_appearance / OSC 11

无用户自定义主题文件;可配的是 pager.toml 布局/动画/块样式。

主会话垂直堆叠

status_bar tasks | catalog | todo ← 二级 pane,非固定左栏 scrollback (Min 5) ← 主 transcript btw | queue | turn_status prompt (动态高度, cap 半屏) shortcuts_bar

缓存边界(bench.md)

StageCached?Hot path?
Markdown parse / syntectYesNo
Word wrapYes (width, gen)No
EntryRenderer + scratchNoYes ~80%
Buffer diff + flushN/AYes
预期 ~50–200 µs/frame(M2 Pro, 120×30, ~200 行 md)。PTY harness 有 p99 回归基线(>15% 失败)。
Why No Fluff

为什么没有一句废话

不是靠单一 “be concise”。是 教模型怎么写 × 给模型精简工具反馈 × 给用户默认折叠的 TUI 三层叠加。用户感知的安静,是过程信息从未以散文进入视野中心。

四层反 fluff 栈

L1 · 模型 prose 宪法
output_efficiency
技术博文 · 比例 · 禁 filler
Codex profile
≤10 行 · 8–12 词 preamble
goal / btw / recap
硬字数合同
L2 · 工具 I/O 压缩
GrokBuildConcise
短结果 · 紧行号
head_limit defaults
20k bash · 200 grep
ToolDefinitionMode
Concise meta-tools
L3 · UI chrome(用户眼睛)
verb-group
Read 3 files
turn_status
1 行 activity
thinking fold
Thought for Xs
L4 · 通道纪律
禁止 bash echo 聊天
prompt 硬规则
todo = progress 主通道
非聊天日记
Harness 已展示勿复述
plan / diff

Prompt 原文(默认路径)

Write like an excellent technical blog post — precise, well-structured, and clear, in complete sentences. Most responses should be concise… Stay focused: avoid filler, repetition, over-the-top detailKeep final responses proportional to task complexity.

crates/codegen/xai-grok-agent/templates/prompt.md · <output_efficiency>

Codex / apply_patch 变体更硬:preamble 1–2 句(8–12 词);进度 ≤8–10 词;默认终答 ≤10 行; “no filler or conversational commentary”;“Do not repeat the full plan — the harness already displays it.”

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 贴进聊天。

content_concise use_concise per-model COMPACT_SYSTEM_PROMPT DEFAULT 20k tool caps grep head_limit=200

UI 默认不展示过程细节

机制默认效果
group_tool_verbstrue连续 read/search → 一行聚合
Thinking displayTruncated → Collapsed推理不占屏
Tool DisplayModeCollapsed成功路径默认 1 行
todo_write3+ steps进度走 pane 不是聊天日记
Tips1 行 + seen cap不堆 toast
Headless -pRUST_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

一句话:「没有一句废话」在用户感知上 = 模型被教成短写(比例 + 禁 filler) + 工具反馈可 concise + TUI 默认折叠/聚合过程信息。 单靠 prompt 做不到;单靠 UI 折叠而模型仍刷屏 preamble 也会脏——两边都做了。

关键源码索引

路径
默认 systemxai-grok-agent/templates/prompt.md
Codex 文风xai-grok-agent/templates/apply_patch_prompt.md
Concise toolsxai-grok-tools/.../grok_build_concise/
use_concisexai-grok-shell/src/agent/config.rs
verb-groupxai-grok-pager/src/scrollback/state/verb_group.rs
turn_statusxai-grok-pager/src/views/turn_status.rs
draw_framexai-grok-pager-render/src/render/draw.rs
streaming MDxai-grok-markdown/src/streaming.rs
event loopxai-grok-pager/src/app/event_loop.rs
perf 架构文xai-grok-pager/benches/bench.md