1. 从一条 Agent 消息说起TUI 到底在消费什么你在终端里敲下一句需求codex cli 的界面开始滚动先出现一行思考状态接着 Agent 的回复逐字冒出来中间夹着命令执行、文件修改、审批弹层。很多人第一次读源码时会以为这是「模型一次性生成完整文本TUI 再整体打印」。实际不是。这些内容来自 App Server 持续推送的通知与请求TUI 只是把它们增量翻译成 Ratatui 组件、终端滚动历史和交互弹层。本篇聚焦 codex cli 源码里 TUI 层消费流式 Agent 事件的实现路径同时给出 TaoToken 统一 Key/API 通道的 settings.json 与 config.toml 配置骨架。读完你应该能画出从AppServerEvent到终端屏幕的完整链路能在本地复现事件消费流程并确认配置真的生效。适合已经读过前几篇、想动手跟做源码链路的同学也适合只想把配置跑通的小白。核心链路先摆出来Core / App Server - AppServerEvent - Thread Event Router - ChatWidget - StreamController / Active Cell - AppEvent - HistoryCell - Terminal Scrollback / Ratatui Viewport几个先给结论当前 TUI 是 App Server Client不是直接持有 Core Session 的特殊前端已提交历史主要写入终端原生 Scrollback正在变化的内容才由 Ratatui Viewport 持续重绘Agent Message 流式渲染采用「稳定区 可变尾部」模型最终合并为保存原始 Markdown 的 Source-backed Cell终端 Scrollback 不是保留式组件树尺寸变化时必须以transcript_cells为真源清屏并重放。还要特别注意协议里存在某个 Delta不等于当前 TUI 已经把它渲染到屏幕。比如AgentMessageDelta会走完整增量渲染CommandExecutionOutputDelta只更新活动 ExecCellFileChangeOutputDelta当前是空实现TurnDiffUpdated只记录 Debug 日志并刷新状态栏。所以本篇会严格区分协议能力、TUI 路由能力、当前可见渲染能力这三层。2. TaoToken 前置统一 Key 与 API 通道在跟做源码之前先把模型通道配好否则 TUI 起来了也没有可用的后端。TaoToken 提供统一的 Key 与 API 通道官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个地址不加 UTM。你需要先拿到一个 API Key。进入控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。生成后复制那串sk-开头的字符串后面配置里会用到。如果你只是想先验证模型通不通可以直接用模型对话页面发一条消息https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。这一步能快速排除 Key 本身的问题再去折腾本地配置会省很多时间。长期跑编码任务或 Agent 场景建议了解 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入细节和字段说明看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Claude Code / Anthropic 兼容通道的说明在https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。注意Key 只放在本地配置文件或环境变量里不要提交到 Git 仓库也不要在截图里露出完整字符串。3. 可复制配置settings.json 与 config.toml 骨架codex cli 的配置分两层一层是 TUI 侧的settings.json管界面与行为开关一层是模型通道侧的config.toml管 provider、base_url、model 这些。下面给的是可复制骨架你按自己的路径和 Key 替换即可。先看settings.json放在 codex 的配置目录下不同平台路径不同通常在用户主目录的.codex下{ tui: { inline_viewport: true, history_wrap_policy: pre_wrap, transcript_reflow_debounce_ms: 75, commit_animation_tick_ms: 16, show_raw_agent_reasoning: false }, streaming: { chunking_mode: smooth, catch_up_queue_threshold: 8, catch_up_age_threshold_ms: 120, exit_queue_threshold: 2, exit_age_threshold_ms: 40 }, history: { resize_reflow_max_rows: 2000 } }这几个字段对应本篇要讲的机制inline_viewport决定已完成历史是否走终端原生 Scrollbackhistory_wrap_policy控制是 TUI 预换行还是交给终端软换行transcript_reflow_debounce_ms就是 Resize 的 75ms 尾部防抖chunking_mode对应 Smooth / CatchUp 两档提交策略resize_reflow_max_rows是长历史重排时的尾部行数上限。再看config.toml这是模型通道配置model gpt-4o-mini model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat [profiles.default] model gpt-4o-mini model_provider taotoken approval_policy on-requestbase_url用不带 UTM 的 API 地址env_key指向环境变量名Key 本身不写进文件。设置环境变量export TAOTOKEN_API_KEYsk-你的KeyWindows PowerShell 用$env:TAOTOKEN_API_KEY sk-你的Key提示wire_api按你实际使用的协议填chat 走对话补全responses 走另一套。不确定时先看文档里的字段表。4. 事件流从 Agent 到 TUI 的传递与渲染配置就绪后进入本篇的核心事件怎么从 Agent 流到屏幕。TUI 通过 App Server Protocol 操作 Thread 和 Turn链路是TUI - AppServerClient - JSON-RPC Request / Notification - App Server - ThreadManager / CodexThread - Session / TurnAppServerSession是 TUI 的 Typed JSON-RPC Facade它不保存 Core Session只负责生成 Request ID、构造 Typed ClientRequest、处理 Embedded/Remote 参数差异、缓存 Bootstrap 结果并把 Thread Response 投影成 TUI Session State。关键入口是next_event()它持续吐出AppServerEvent。App::run里最关键的是四路异步事件循环let control select! { Some(event) app_event_rx.recv() { /* 内部 AppEvent */ } active async { if let Some(rx) app.active_thread_rx.as_mut() { rx.recv().await } else { None } }, if App::should_handle_active_thread_events(...) { /* 活动 Thread 缓冲事件 */ } event tui_events.next() { /* Crossterm / TuiEvent */ } app_server_event app_server.next_event(), if listen_for_app_server_events { /* AppServerEvent */ } };四路输入分别是内部 AppEvent、活动 Thread 缓冲事件、终端 TuiEvent、AppServerEvent。多智能体场景下App Server 会同时发多条 Thread 的事件所以必须先按 ThreadId 分类否则子 Agent 的命令输出可能进入主 Agent 的 active_cell。分类函数是server_notification_thread_target目标类型有Thread(ThreadId)、InvalidThreadId、AppScoped、Global。每条 Thread 有独立的ThreadEventStore保存 Session Snapshot、Turn Snapshot、Live Event Buffer、Pending Approval/Input State、Active Turn ID、Composer/Input Snapshot。活动 Thread 走有界 mpsc Channel 立即更新 ChatWidget非活动 Thread 的事件进 Replay Buffer用户切回时再重建 Widget 并重放 Snapshot。这就是「单活动视图 多会话状态缓存」而不是每个子 Agent 一个并行 Ratatui Widget Tree。Agent Message 的完整调用链是这样的ServerNotification::AgentMessageDelta - ChatWidget::handle_server_notification - ChatWidget::on_agent_message_delta - ChatWidget::handle_streaming_delta - StreamController::push - MarkdownStreamCollector::push_delta - commit_complete_source - render_markdown_agent_with_links_and_cwd - stable queue / live tail稳定行随后经过AppEvent::StartCommitAnimation、AppEvent::CommitTick、ChatWidget::on_commit_tick、streaming::run_commit_tick生成AgentMessageCell再通过AppEvent::InsertHistoryCell写入终端 Scrollback。最终完成时ItemCompleted::AgentMessage触发finalize_completed_assistant_messageStreamController::finalize后发送AppEvent::ConsolidateAgentMessage把多个临时 Cell 合并成一个AgentMarkdownCell。这里有个关键设计MarkdownStreamCollector只按换行门控提交不做 Markdown 解析。核心规则是let commit_end self.buffer.rfind(\n).map(|idx| idx 1)?;只有最后一个换行之前的 Source 才能进入下一阶段。不含换行的 Delta 追加到 Buffer等待后续 Delta。这样能显著降低半个列表标记、半个标题、半个表格行导致的闪烁。Streaming 用两区模型Stable Region 可以按顺序写入 ScrollbackMutable Tail 仍可能被后续 Markdown 改写留在 active_cell。索引关系是emitted_stable_len enqueued_stable_len rendered_lines.len()。Mutable Tail 从enqueued_stable_len开始而不是从emitted_stable_len开始否则已入队但尚未输出的行会同时出现在 Commit Queue 和 Active Tail造成重复显示。表格需要 Holdback。Markdown 表格不是天然可增量提交的新增行可能改变每一列宽度和前面所有行的换行。table_holdback.rs用状态机检测 Header Row 紧跟 Delimiter Row从表头开始的区域都保留在 Mutable Tail。扫描器跳过非 Markdown Fence 内部的管道字符比如let x a | b;不会被误识别成表格。Commit Animation 由 AppEvent 驱动App 用标准线程定期发送AppEvent::CommitTick间隔是tui::TARGET_FRAME_INTERVAL。动画线程只负责定时真正的队列决策仍在主 UI 事件序列中执行避免后台线程直接修改 Widget 状态。Smooth 模式每个 Tick 输出一行CatchUp 模式一个 Tick 批量清空当前队列。进入 CatchUp 的阈值是queued_lines 8或oldest_age 120ms退出前要求queued_lines 2且oldest_age 40ms持续 250ms。这是带 Hysteresis 的背压策略。5. 验证请求与成功结果配置和链路都清楚了现在动手验证。第一步确认环境变量生效echo $TAOTOKEN_API_KEY应该输出你的 Key。第二步用 curl 直接打 API排除 TUI 层干扰curl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 只回复两个字通了}], stream: true }如果返回里能看到data:开头的流式分片说明 Key 和通道都正常。第三步启动 codex cli观察 TUI 是否消费到流式事件codex --profile default进入界面后输入一句会触发多行输出的需求比如让它写一个带表格的 Markdown。你应该能看到普通段落逐步提交表格从表头开始留在可变尾部流结束后临时 Cell 被AgentMarkdownCell替换。这时拖动终端窗口改变宽度历史会按新宽度重排而不是保留旧换行。验证事件消费是否真的走了流式路径可以在源码里加日志点。建议断点位置chatwidget/protocol.rs - AgentMessageDelta chatwidget/streaming.rs - handle_streaming_delta streaming/controller.rs - StreamCore::push_delta - sync_stable_queue streaming/commit_tick.rs - run_commit_tick app/event_dispatch.rs - InsertHistoryCell - ConsolidateAgentMessage app/agent_message_consolidation.rs - handle_consolidate_agent_message观察这几个状态raw_source.len()、rendered_lines.len()、emitted_stable_len、enqueued_stable_len、queued_lines、TableHoldbackState、active_cell类型、transcript_cells末尾类型。预期结果是普通段落逐步提交表格从表头开始留在 Mutable Tail流结束后临时 AgentMessageCell 被 AgentMarkdownCell 替换Resize 后从原始 Markdown 重排。6. 本篇常见错排查Agent 文本顺序异常先检查事件是否路由到正确 ThreadId再检查stream_controller是否仍存在然后看interrupts队列是否被 Flush接着核对emitted/enqueued/rendered三个长度最后确认ConsolidateAgentMessage是否执行。命令输出不更新检查CommandExecutionOutputDelta.item_id确认active_cell是否为 ExecCell确认 ExecCell 中是否存在同一call_id看append_output是否返回 true最后检查active_cell_revision是否递增。Patch 不显示检查ItemStarted::FileChange是否包含 changes不要只等待FileChangeOutputDelta当前是空实现检查file_update_changes_to_display转换确认PatchHistoryCell是否进入 AppEvent 队列再看 Overlay 是否暂存了 History Insert。Resize 后历史错位检查last_observed_width和last_reflow_width看 Pending Reflow Deadline 是否被正确安排确认 Overlay 是否阻塞 Reflow检查 Agent Stream 是否已 Consolidate最后确认 Cell 是否保存 Source 而不是旧 Rendered Line。Resume 后审批重复检查PendingInteractiveReplayState确认 Outbound Approval Op 是否记入 Store检查ServerRequestResolved看 Buffer 淘汰是否同步 Pending State最后确认ReplayKind是否为ThreadSnapshot。Snapshot 不稳定固定终端宽高固定时间参考禁用或冻结动画归一化临时路径避免依赖 HashMap 非稳定顺序检查终端颜色环境变量。配置不生效确认TAOTOKEN_API_KEY在当前 shell 可见确认config.toml里base_url用的是不带 UTM 的 API 地址确认model_provider名字和[model_providers.taotoken]段名一致。如果 TUI 起来了但请求报 401多半是 Key 没读到或环境变量名写错。7. 继续深入与接入入口到这里从 App Server 事件到终端屏幕的链路已经完整AppServerSession - Typed JSON-RPC - AppServerEvent - Thread Target Classification - ThreadEventStore / Channel - Active ChatWidget - Stream / Tool Lifecycle - HistoryCell - Scrollback Ratatui Viewport。Agent Message 的核心路径是 Delta 经换行门控、完整 Markdown 重渲染、稳定区加可变尾部、Smooth/CatchUp 提交最后 Consolidate 成AgentMarkdownCell。如果你在排障或接入阶段卡住优先看 API Keys 和接入文档https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 与 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。想先验证模型通不通用模型对话页面最快https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。长期跑编码和 Agent 任务Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。下一篇会离开交互式 TUI分析无交互执行模式以及两套 SDK 如何复用同一套 App Server 和协议能力。在那之前建议你先把本篇的配置骨架跑通再挑一个 Delta 断点跟一遍亲手看到AgentMarkdownCell替换临时 Cell 的那一刻比读十遍源码都管用。
