Codex 系统手册

流式汇总与 UI 展示

stage-13.251 个文件

这一阶段是在程序干活时,把后台不断冒出来的内容变成用户看得懂的聊天界面。前面先像筛子一样解析流式文字,藏起引用、计划、Git 指令等特殊标记,只留下干净正文。中间把 Markdown、表格、代码高亮、diff 和命令输出排好版,窗口变宽变窄也能重排。后面把临时滚动的回答、计划、审批、搜索、工具调用、状态栏和 token 用量,整理成正式历史记录。它就像直播字幕组,一边接收,一边校对排版,最后归档。

本阶段的文件51

流式解析与 markdown 管线

这些文件定义如何解析、缓冲、渲染增量式助手输出为 markdown,并在其足够稳定之前安全地暂缓公开。

utils/stream-parser/src/citation.rs源码 ↗
domain_logiccross-cutting / stream parsing

这份代码解决的是“文字不是一次性给完,而是一小块一小块到来”时的引用解析问题。引用标签可能被切在两块数据中间,比如上一块只有 <oai-mem-,下一块才补完 citation>。如果直接按整段字符串找,很容易漏掉。这里用 CitationStreamParser 包了一层通用的 InlineHiddenTagParser(内联隐藏标签解析器,可以识别正文里的特殊标签并把它们从可见文字中拿掉)。它只认一种标签:<oai-mem-citation></oai-mem-citation>。解析时,标签本身不会出现在可见文字里,标签里的内容会作为引用列表返回。到结尾时如果引用没写关闭标签,它也会自动收尾,把已经攒到的内容当作引用。要注意,它按字面匹配,不支持标签套标签。

函数细节13
CitationStreamParser::new28–36 ↗
fn new() -> Self

作用:创建一个新的引用流解析器。有人要开始从文字流里剥离引用标签时,就先调用它。

数据流:进去不需要外部输入 → 它准备好一条规则:看到 <oai-mem-citation> 就开始隐藏并收集,看到 </oai-mem-citation> 就结束 → 出来一个可以连续接收文字块的 CitationStreamParser

调用关系:这是整个解析流程的起点。strip_citations 会用它处理完整字符串,多个测试也会用它直接检查流式输入、半截标签和未闭合标签这些情况。它把真正识别标签的活交给底层的 InlineHiddenTagParser::new

调用图:调用 1 个内部函数(new);被 11 处调用(strip_citations, citation_parser_auto_closes_unterminated_tag_on_finish, citation_parser_buffers_partial_open_tag_prefix, citation_parser_preserves_partial_open_tag_at_eof_if_not_a_full_tag, citation_parser_streams_across_chunk_boundaries, utf8_stream_parser_errors_on_incomplete_code_point_at_eof, utf8_stream_parser_handles_split_code_points_across_chunks, utf8_stream_parser_into_inner_errors_when_partial_code_point_is_buffered, utf8_stream_parser_into_inner_lossy_drops_buffered_partial_code_point, utf8_stream_parser_rolls_back_entire_chunk_when_invalid_byte_follows_valid_prefix (+1 more));外部调用 1 个(vec!)。

CitationStreamParser::default40–42 ↗
fn default() -> Self

作用:提供一种标准的默认创建方式,效果和直接调用 CitationStreamParser::new 一样。

数据流:进去没有参数 → 它转手调用 new → 出来一个按默认引用标签规则配置好的解析器。

调用关系:这是 Rust 里常见的默认构造入口。需要泛型代码或工具函数用“默认值”创建解析器时,可以走这里;实际配置仍然由 CitationStreamParser::new 完成。

调用图:外部调用 1 个(new)。

CitationStreamParser::push_str48–54 ↗
fn push_str(&mut self, chunk: &str) -> StreamTextChunk<Self::Extracted>

作用:把新到来的一小段文字喂给解析器,并立刻拿回这一小段能确定的可见文字和引用内容。

数据流:进去是一段字符串切片 chunk,同时读取解析器内部之前攒下的状态 → 它交给内部解析器识别引用标签,把标签从可见文字中去掉,把标签里的内容提出来 → 出来一个 StreamTextChunk,里面有可显示的正文和新提取到的引用字符串;解析器内部状态也会更新,方便下一块文字继续接上。

调用关系:这是流式解析的主要工作口。外层的流式文本处理会反复调用它;它自己不重新发明识别规则,而是调用内部 InlineHiddenTagParserpush_str,再把带标签信息的结果简化成普通字符串引用。

调用图:调用 1 个内部函数(push_str);被 1 处调用(push_str)。

CitationStreamParser::finish56–62 ↗
fn finish(&mut self) -> StreamTextChunk<Self::Extracted>

作用:告诉解析器“文字已经没有后续了”,让它把最后攒着的内容结算出来。

数据流:进去没有新文字,但会读取解析器内部还没吐出的缓存 → 它让内部解析器完成收尾;如果已经进入引用标签但没等到关闭标签,就按文件约定自动关闭 → 出来最后一批可见文字和引用内容。

调用关系:它通常在所有 push_str 调用之后执行。没有这一步,末尾半截普通文字或未闭合引用可能还留在缓存里。它把收尾细节交给内部解析器的 finish

调用图:调用 1 个内部函数(finish);被 1 处调用(finish)。

strip_citations69–76 ↗
fn strip_citations(text: &str) -> (String, Vec<String>)

作用:处理一整段已经拿到手的文字,直接返回“去掉引用标签后的正文”和“引用内容列表”。这是给不需要流式喂数据的调用方用的快捷函数。

数据流:进去是一整段 text → 它新建一个 CitationStreamParser,把整段文字一次性喂进去,再调用 finish 做收尾,然后把两次产出的正文和引用合并 → 出来一个二元结果:干净正文 String 和引用 Vec<String>

调用关系:这是便利入口,内部仍然复用 CitationStreamParser::new 和同一套流式语义,所以行为和逐块解析一致。相关测试用它验证多条引用、未闭合引用、以及不支持嵌套标签的表现。

调用图:调用 1 个内部函数(new);被 3 处调用(citation_parser_does_not_support_nested_tags, strip_citations_auto_closes_unterminated_citation_at_eof, strip_citations_collects_all_citations)。

tests::collect_chunks86–100 ↗
fn collect_chunks(parser: &mut P, chunks: &[&str]) -> StreamTextChunk<P::Extracted>

作用:测试用的小帮手:把多块输入依次喂给某个流式解析器,并把每次输出合并成一个总结果。

数据流:进去是一个可变解析器和一组字符串块 → 它先建一个空的 StreamTextChunk,逐块调用解析器的 push_str,把可见文字接起来,把提取结果追加起来,最后再调用 finish → 出来一份完整汇总后的解析结果。

调用关系:它只在测试里用,目的是让测试更像真实流式场景。多个引用解析测试会借它模拟“数据被切成几段到达”的情况,它调用的是解析器接口里的 push_strfinish

调用图:调用 1 个内部函数(default);外部调用 2 个(finish, push_str)。

tests::citation_parser_streams_across_chunk_boundaries103–116 ↗
fn citation_parser_streams_across_chunk_boundaries()

作用:验证引用标签就算被拆在不同文字块里,解析器也能认出来。

数据流:进去是一组故意切开的字符串块,其中打开标签和关闭标签都跨块断开 → 测试把这些块交给 collect_chunks → 期望出来的正文是 Hello world,引用列表里只有 source A

调用关系:这个测试先用 CitationStreamParser::new 创建解析器,再用 collect_chunks 模拟连续输入。它证明 push_str 不是只看当前块,而是会记住上一块留下的半截标签。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::citation_parser_buffers_partial_open_tag_prefix119–132 ↗
fn citation_parser_buffers_partial_open_tag_prefix()

作用:验证当输入末尾像是引用开始标签的一部分时,解析器会先等一等,不会急着把它当普通文字吐出去。

数据流:第一次进去 abc <oai-mem- → 解析器输出 abc ,把半截标签暂存;第二次进去补全的 citation>x</oai-mem-citation>z → 解析器识别出引用 x,输出后面的 z;最后 finish 没有额外内容。

调用关系:这个测试直接调用 CitationStreamParser::newpush_strfinish。它盯住的是流式解析里很容易出错的地方:半截标签必须缓存,不能提前误判。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert!, assert_eq!)。

tests::citation_parser_auto_closes_unterminated_tag_on_finish135–141 ↗
fn citation_parser_auto_closes_unterminated_tag_on_finish()

作用:验证引用标签打开后如果一直没有关闭,到输入结束时也会把里面的内容当作引用取出来。

数据流:进去的文字是 x<oai-mem-citation>source,没有关闭标签 → collect_chunks 喂完后调用 finish → 出来的可见正文是 x,引用列表是 source

调用关系:这个测试通过 CitationStreamParser::newcollect_chunks 走完整流式流程。它确认 finish 的收尾行为:遇到未闭合引用时自动收口,而不是丢掉内容。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::citation_parser_preserves_partial_open_tag_at_eof_if_not_a_full_tag144–150 ↗
fn citation_parser_preserves_partial_open_tag_at_eof_if_not_a_full_tag()

作用:验证如果结尾只是一个像标签开头的半截文字,但并没有组成完整引用标签,它会被当作普通正文保留下来。

数据流:进去的文字是 hello <oai-mem- → 解析器等到 finish 时发现它并不是完整的 <oai-mem-citation> → 出来的可见正文仍是 hello <oai-mem-,引用列表为空。

调用关系:这个测试同样通过 CitationStreamParser::newcollect_chunks 运行。它补充了上一个未闭合引用测试:只有真正进入了完整引用标签,结尾才会自动关闭;普通半截文字不能被误删。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::strip_citations_collects_all_citations153–160 ↗
fn strip_citations_collects_all_citations()

作用:验证快捷函数 strip_citations 能从一整段文字里收集多条引用,并把正文拼干净。

数据流:进去是一段夹着两组引用标签的字符串 → strip_citations 去掉两组标签和标签内容,同时收集 onetwo → 出来的可见正文是 abc,引用列表是 onetwo

调用关系:这个测试不直接操作流式解析器,而是检查完整字符串入口 strip_citations。它保证快捷入口没有漏掉多次出现的引用。

调用图:调用 1 个内部函数(strip_citations);外部调用 1 个(assert_eq!)。

tests::strip_citations_auto_closes_unterminated_citation_at_eof163–168 ↗
fn strip_citations_auto_closes_unterminated_citation_at_eof()

作用:验证快捷函数遇到没有关闭标签的引用时,也和流式解析器一样会在结尾自动收口。

数据流:进去是 x<oai-mem-citation>ystrip_citations 内部解析并收尾 → 出来正文 x,引用列表 y

调用关系:这个测试调用 strip_citations,间接覆盖它内部的 CitationStreamParser::finish 行为。它说明快捷函数继承了流式解析器的结尾规则。

调用图:调用 1 个内部函数(strip_citations);外部调用 1 个(assert_eq!)。

tests::citation_parser_does_not_support_nested_tags171–178 ↗
fn citation_parser_does_not_support_nested_tags()

作用:验证这个解析器不支持引用标签套引用标签,并把这种限制固定下来,避免以后有人误以为它会处理嵌套结构。

数据流:进去是一段外层引用里又放了一个引用标签的文字 → strip_citations 按“遇到第一个关闭标签就结束”的简单规则处理 → 出来的引用是 x<oai-mem-citation>y,剩下的 z</oai-mem-citation>b 留在可见正文里。

调用关系:这个测试调用 strip_citations。它记录的是重要边界:解析器按字面、非嵌套方式工作,适合简单隐藏标签,不适合像 HTML 那样解析复杂层级。

调用图:调用 1 个内部函数(strip_citations);外部调用 1 个(assert_eq!)。

utils/stream-parser/src/proposed_plan.rs源码 ↗
domain_logicstream parsing / response handling

这个文件解决的是“边收到文字边拆分内容”的问题。模型的回复可能不是一次性给完,而是一小段一小段流出来,甚至 <proposed_plan> 这个标签本身都可能被拆成两半。ProposedPlanParser 就像一个会看标记的分拣员:普通文字放进 visible_text,计划块开始时发出 ProposedPlanStart,计划内容一段段发出 ProposedPlanDelta,结束时发出 ProposedPlanEnd。底层真正识别标签的是 TaggedLineParser,这里把它识别出的通用标签结果,翻译成“计划块”专用的结果。文件还提供两个方便函数:一个直接删除计划块,只留下可见文字;另一个直接提取计划文字。一个重要行为是:如果计划块开了但没有正常关闭,finish 会在收尾时帮它补成结束,避免上层一直以为还没结束。

函数细节13
ProposedPlanParser::new34–42 ↗
fn new() -> Self

作用:创建一个新的计划块解析器。它告诉底层解析器:只需要认 <proposed_plan></proposed_plan> 这一对标签。

数据流:进去没有业务数据 → 它准备一条标签规则,说明开始标签、结束标签,以及这个标签代表“计划块” → 出来一个干净的新 ProposedPlanParser,可以马上接收流式文字。

调用关系:这是整个文件的起点。strip_proposed_plan_blocksextract_proposed_plan_text 和多个测试都会先调用它造出解析器;它内部把具体识别工作交给 TaggedLineParser::new

调用图:调用 1 个内部函数(new);被 5 处调用(extract_proposed_plan_text, strip_proposed_plan_blocks, closes_unterminated_plan_block_on_finish, preserves_non_tag_lines, streams_proposed_plan_segments_and_visible_text);外部调用 1 个(vec!)。

ProposedPlanParser::default46–48 ↗
fn default() -> Self

作用:提供一种默认创建方式,让别人可以用 Rust 的默认值习惯来得到一个计划块解析器。

数据流:进去没有参数 → 它直接调用 ProposedPlanParser::new → 出来一个和手动 new 一样的解析器。

调用关系:它是 new 的简写入口。需要默认构造解析器的代码会走这里,而真正的初始化规则仍然集中在 ProposedPlanParser::new

调用图:外部调用 1 个(new)。

ProposedPlanParser::push_str54–56 ↗
fn push_str(&mut self, chunk: &str) -> StreamTextChunk<Self::Extracted>

作用:把新收到的一小段文字喂给解析器,并立刻拿到这小段里能确定的结果。它适合处理流式输出,也就是文字一块一块到来的情况。

数据流:进去一段新文字 chunk,以及解析器之前记住的未完成状态 → 它先让底层 parse 判断哪些是普通文字、哪些是标签或标签内容,再用 map_segments 翻译成计划块专用结果 → 出来一个 StreamTextChunk,里面有可直接显示的文字和按顺序记录的计划相关片段。

调用关系:它是外部持续喂数据时最常用的入口。调用图里它会被 parse_visible_text 这类上层解析流程使用;它自己把识别交给底层 parse,再把格式转换交给 map_segments

调用图:调用 2 个内部函数(map_segments, parse);被 1 处调用(parse_visible_text)。

ProposedPlanParser::finish58–60 ↗
fn finish(&mut self) -> StreamTextChunk<Self::Extracted>

作用:告诉解析器“没有更多文字了”,让它吐出最后还压在内部的内容。没有这一步,末尾没凑齐的标签或未关闭的计划块可能不会被正确处理。

数据流:进去的是解析器当前记住的剩余状态 → 它调用底层 finish 做收尾,比如处理未关闭的块,再用 map_segments 转成计划块结果 → 出来最后一批 StreamTextChunk,包括可能补出的 ProposedPlanEnd

调用关系:它在一整段流式文本结束时使用。调用图里它被上层的 finish 流程使用;内部仍然是先让通用标签解析器收尾,再交给 map_segments 做专用翻译。

调用图:调用 2 个内部函数(map_segments, finish);被 1 处调用(finish)。

map_segments63–84 ↗
fn map_segments(segments: Vec<TaggedLineSegment<PlanTag>>) -> StreamTextChunk<ProposedPlanSegment>

作用:把底层通用标签解析器的结果,翻译成这个文件专用的计划块结果。它还顺手把普通文字拼进 visible_text,让上层知道哪些内容可以显示。

数据流:进去一串 TaggedLineSegment,可能是普通文字、标签开始、标签内容、标签结束 → 它逐个改名成 ProposedPlanSegment,普通文字会同时加入可见文本,计划块内容则只进入提取结果 → 出来一个 StreamTextChunk,同时包含“可见正文”和“完整片段记录”。

调用关系push_strfinish 都会调用它。它处在底层识别和上层使用之间,像翻译员一样,把通用的标签事件翻译成“计划块开始、计划块内容、计划块结束”。

调用图:调用 1 个内部函数(default);被 2 处调用(finish, push_str);外部调用 2 个(Normal, ProposedPlanDelta)。

strip_proposed_plan_blocks86–91 ↗
fn strip_proposed_plan_blocks(text: &str) -> String

作用:从一整段文字里删掉 <proposed_plan> 计划块,只返回普通可见文字。适合上层只想展示最终回复、不想展示计划草稿时使用。

数据流:进去完整文本 → 它创建一个 ProposedPlanParser,把文本喂进去,再做一次收尾 → 出来一个字符串,里面已经去掉计划块和计划块内容,只留下普通正文。

调用关系:这是给调用者用的快捷工具,不需要调用者自己处理流式片段。它先通过 ProposedPlanParser::new 建解析器,然后利用解析器产出的 visible_text 得到最终结果。

调用图:调用 1 个内部函数(new)。

extract_proposed_plan_text93–115 ↗
fn extract_proposed_plan_text(text: &str) -> Option<String>

作用:从一整段文字里提取计划块中的文字。如果没有看到计划块,就返回空结果。

数据流:进去完整文本 → 它创建解析器,读取所有解析片段;看到 ProposedPlanStart 就记下“确实有计划块”并清空旧内容,看到 ProposedPlanDelta 就把内容追加进去,普通文字和结束标记不加入 → 出来 Some(计划文字),如果没见过计划块就出来 None

调用关系:这是另一个快捷工具,服务于只关心计划内容的场景。它调用 ProposedPlanParser::new 建立解析器,然后消费解析器产生的片段,自己负责把计划文字拼起来。

调用图:调用 1 个内部函数(new);外部调用 1 个(new)。

tests::collect_chunks127–141 ↗
fn collect_chunks(parser: &mut P, chunks: &[&str]) -> StreamTextChunk<P::Extracted>

作用:测试用的小帮手,把多段输入依次喂给某个流式解析器,并把每次吐出的结果合并起来。这样测试可以模拟真实网络流一样的“分块到达”。

数据流:进去一个解析器和若干文字块 → 它逐块调用 push_str,把每次的可见文字和提取片段累加,最后调用 finish 收尾 → 出来一份合并后的 StreamTextChunk,方便断言检查。

调用关系:它只在测试模块里使用。多个测试函数把不同切法的文字交给它,借此验证 ProposedPlanParser 在分块输入和收尾时是否稳定。

调用图:调用 1 个内部函数(default);外部调用 2 个(finish, push_str)。

tests::streams_proposed_plan_segments_and_visible_text144–166 ↗
fn streams_proposed_plan_segments_and_visible_text()

作用:验证解析器能在标签被拆开的情况下,仍然正确识别计划块,并把可见正文和计划片段分开。

数据流:进去的是几段故意切开的文字,比如 <proposed_plan> 被拆成两块 → 测试用 collect_chunks 喂给解析器 → 结果应当是可见文字只剩开头和结尾,提取片段里按顺序出现普通文字、计划开始、计划内容、计划结束、普通文字。

调用关系:这个测试先调用 ProposedPlanParser::new 建解析器,再交给 tests::collect_chunks 模拟流式输入,最后用 assert_eq! 检查输出是否符合预期。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::preserves_non_tag_lines169–180 ↗
fn preserves_non_tag_lines()

作用:验证看起来有点像标签、但不是独立合法标签的行,不会被误删。比如前面有空格、后面有额外文字的 <proposed_plan> 应该当普通文字处理。

数据流:进去一行 <proposed_plan> extra\n → 解析器处理后不应该进入计划模式 → 出来的可见文字保持原样,提取片段也只是一个普通文字片段。

调用关系:这个测试同样通过 ProposedPlanParser::newtests::collect_chunks 运行解析流程,最后用 assert_eq! 保证解析器不会过度敏感、误伤正常文本。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::closes_unterminated_plan_block_on_finish183–196 ↗
fn closes_unterminated_plan_block_on_finish()

作用:验证计划块如果开始了但文本结束前没有写关闭标签,解析器在 finish 时也会自动给出结束事件。

数据流:进去一段只有 <proposed_plan> 开始和计划内容、没有 </proposed_plan> 结束的文字 → collect_chunks 在最后调用 finish → 出来的片段里仍然有计划开始、计划内容、计划结束,可见文字为空。

调用关系:这个测试关注收尾行为。它用 ProposedPlanParser::new 创建解析器,借 tests::collect_chunks 触发最后的 finish,再用 assert_eq! 检查未关闭块是否被安全收束。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, collect_chunks)。

tests::strips_proposed_plan_blocks_from_text199–202 ↗
fn strips_proposed_plan_blocks_from_text()

作用:验证快捷函数 strip_proposed_plan_blocks 真的能把计划块整段删掉,只留下前后的普通文字。

数据流:进去一段包含 before、计划块、after 的完整文本 → 调用被测试的删除计划块函数 → 期望出来的字符串是 before\nafter

调用关系:这是面向便捷工具的测试。它不直接操纵解析器,而是用 assert_eq! 检查 strip_proposed_plan_blocks 对外表现是否正确。

调用图:外部调用 1 个(assert_eq!)。

tests::extracts_proposed_plan_text205–211 ↗
fn extracts_proposed_plan_text()

作用:验证快捷函数 extract_proposed_plan_text 能把计划块里的正文取出来,而不是取到外面的普通文字。

数据流:进去一段包含普通文字和计划块的完整文本 → 调用提取函数 → 期望出来 Some("- step\n"),表示确实找到计划块并拿到了里面的内容。

调用关系:这是另一个便捷工具的测试。它通过 assert_eq! 检查 extract_proposed_plan_text 的最终返回值,保证上层以后可以放心直接用这个函数拿计划内容。

调用图:外部调用 1 个(assert_eq!)。

utils/stream-parser/src/assistant_text.rs源码 ↗
domain_logicrequest handling / streaming response parsing

助手的回复不是一次性完整到达,而是像水龙头滴水一样一小段一小段流过来。这里的解析器就像一个边读边筛的过滤网:先把 <oai-mem-citation> 这种引用标签拿掉,把里面的引用内容单独收集起来;如果打开了 plan mode(计划模式,也就是需要识别 <proposed_plan> 块),再把计划块从普通可见文字里拆出来,变成结构化的计划片段。最重要的是,它能处理“标签被切开”的情况,比如上一段只有 <proposed,下一段才有 _plan>,解析器会记住前面的半截,不会误显示。AssistantTextChunk 是每次解析吐出的结果包,里面有可见文字、引用列表、计划片段。AssistantTextStreamParser 是真正工作的机器,内部串起引用解析器和计划解析器,最后用 finish 把残留内容收尾。

函数细节7
AssistantTextChunk::is_empty15–17 ↗
fn is_empty(&self) -> bool

作用:判断这次解析结果是不是完全没有东西。别人可以用它来决定要不要继续处理、显示,或者发送这个空结果。

数据流:进去的是一个 AssistantTextChunk 自己已有的三份内容:可见文字、引用列表、计划片段 → 它检查这三份是不是都为空 → 出来一个真假值;它不会改动任何数据。

调用关系:它是结果包上的小检查工具,常用于收尾后判断还有没有剩余内容。文件里的计划解析测试最后就用它确认 finish 没有再吐出多余东西。

AssistantTextStreamParser::new31–36 ↗
fn new(plan_mode: bool) -> Self

作用:创建一个新的助手文本流解析器,并决定要不要开启“计划模式”。计划模式开启后,它会额外识别 <proposed_plan> 里的内容。

数据流:进去的是一个布尔值 plan_mode,意思是是否解析计划块 → 它用默认值准备好内部的引用解析器和计划解析器,再把 plan_mode 存进去 → 出来一个可以持续接收文本片段的新解析器。

调用关系:这是使用解析器的起点。在这个文件的两个测试 tests::parses_citations_across_seed_and_delta_boundariestests::parses_plan_segments_after_citation_stripping 里,都会先调用它搭好解析器。它内部依赖默认初始化来准备其余零件。

调用图:被 2 处调用(parses_citations_across_seed_and_delta_boundaries, parses_plan_segments_after_citation_stripping);外部调用 1 个(default)。

AssistantTextStreamParser::push_str38–43 ↗
fn push_str(&mut self, chunk: &str) -> AssistantTextChunk

作用:把新到的一小段助手文本喂给解析器,立刻拿到这一段里已经能确定的可见文字、引用和计划片段。

数据流:进去的是一段新文本 chunk → 它先交给引用解析器的 push_str,把引用标签剥掉并拿到引用内容;然后把剥完引用后的可见文字交给 parse_visible_text,必要时再解析计划块 → 出来一个 AssistantTextChunk,其中包含这次能显示的文字、这次抽出的引用、以及计划片段;同时解析器内部会记住还没闭合的半截标签。

调用关系:它是流式解析的日常入口:上层每收到一段新文字,就该调用它一次。它自己不包办所有细节,而是先把引用处理交给引用解析器,再把剩下的可见文字交给 AssistantTextStreamParser::parse_visible_text

调用图:调用 2 个内部函数(parse_visible_text, push_str)。

AssistantTextStreamParser::finish45–57 ↗
fn finish(&mut self) -> AssistantTextChunk

作用:告诉解析器“文本流结束了”,让它把还攒在内部、之前不能确定的尾巴内容吐出来。

数据流:进去时不需要新文本,但会读取解析器内部缓存的半截内容 → 它先让引用解析器 finish,拿到最后剩下的可见文字和引用;再调用 parse_visible_text 处理这段文字;如果开启了计划模式,还会让计划解析器 finish,把计划解析器里最后的尾巴合并进结果 → 出来一个最终的 AssistantTextChunk,并完成内部收尾。

调用关系:它用在流结束时,是 push_str 的收尾搭档。它会调用 AssistantTextStreamParser::parse_visible_text,也会调用内部引用解析器和计划解析器各自的 finish,确保没有内容卡在半路。

调用图:调用 3 个内部函数(parse_visible_text, finish, finish)。

AssistantTextStreamParser::parse_visible_text59–72 ↗
fn parse_visible_text(&mut self, visible_text: String) -> AssistantTextChunk

作用:处理已经去掉引用标签的文字:如果没开计划模式,就原样当可见文字返回;如果开了,就继续识别计划块并拆出计划片段。

数据流:进去的是一段已经剥掉引用的 visible_text → 如果 plan_mode 为假,它直接包装成结果返回;如果 plan_mode 为真,它把文字交给计划解析器的 push_str,得到真正该显示的文字和计划片段 → 出来一个 AssistantTextChunk,通常不包含引用,因为引用已经在前一步处理完了。

调用关系:它是引用解析之后的第二道过滤网。AssistantTextStreamParser::push_strAssistantTextStreamParser::finish 都会调用它,用同一套规则处理普通流入内容和最后收尾内容。

调用图:调用 1 个内部函数(push_str);被 2 处调用(finish, push_str);外部调用 1 个(default)。

tests::parses_citations_across_seed_and_delta_boundaries82–95 ↗
fn parses_citations_across_seed_and_delta_boundaries()

作用:测试引用标签被拆成两段发来时,解析器仍然能正确隐藏标签并提取引用。

数据流:进去的是测试里手写的两段文本:第一段包含引用标签开头和半个引用,第二段补完引用并接上普通文字 → 测试创建不开计划模式的解析器,依次调用 push_strfinish → 检查结果是:用户只看到正常文字,引用 doc1 被单独提取,结尾没有多余内容。

调用关系:它通过 AssistantTextStreamParser::new 建立解析器,然后用断言检查行为。这个测试保护的是引用解析的关键场景:特殊标签跨越流式边界时不能出错。

调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。

tests::parses_plan_segments_after_citation_stripping98–129 ↗
fn parses_plan_segments_after_citation_stripping()

作用:测试在计划模式下,解析器会先去掉引用,再正确识别 <proposed_plan> 计划块。

数据流:进去的是几段故意拆开的文本:有普通开头、被拆开的计划标签、计划内容里的引用、结束标签和结尾文字 → 测试创建开启计划模式的解析器,连续喂入这些片段并最终 finish → 检查结果是:引用被抽走,计划开始、计划内容、计划结束、普通文字都按顺序变成计划片段,可见文字里不混入计划标签。

调用关系:它同样从 AssistantTextStreamParser::new 开始,用断言确认引用解析和计划解析这两层过滤能正确配合。它特别证明了处理顺序很重要:先剥引用,再拆计划。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert!, assert_eq!)。

tui/src/streaming/table_holdback.rs源码 ↗
domain_logicmain loop / streaming update

聊天或代理输出是一个字一个块慢慢来的,Markdown 表格也是这样来的。问题是:表格只有看到“表头行”和下一行“分隔线”后,才知道它真的是表格;而表格一旦继续加行,前面的列宽可能也要跟着变。如果太早把表格画死到历史区,就像账本刚写一半就装订,后面改不了。这个文件用一个很保守的小扫描器记录原始文本的位置,只判断“这里可能是表格开头”或“已经确认是表格”。它还会避开普通代码块里的竖线,避免把代码误认成表格;但允许引用块里的表格。真正怎么画表格不在这里做,它只告诉上层:从哪个位置开始要留在可变尾巴里。

函数细节8
TableHoldbackScanner::new60–68 ↗
fn new() -> Self

作用:创建一个全新的表格延后扫描器。它用于从一段还没扫描过的流式文本开头开始,重新判断是否有 Markdown 表格正在形成。

数据流:进去不需要任何输入 → 它把当前读到的源文本位置设为 0,创建代码块追踪器,并清空“上一行”“待确认表头”“已确认表格”等记忆 → 出来一个干净的 TableHoldbackScanner,可以开始按顺序喂入文本块。

调用关系:它是这个扫描器的起点。测试和外层构造流程会调用它;reset 也会借它的逻辑把旧扫描器恢复成刚创建时的状态。内部还会创建 FenceTracker,用来判断当前行是不是在代码块里。

调用图:调用 1 个内部函数(new);被 3 处调用(new, incremental_holdback_detects_header_delimiter_across_chunk_boundary, incremental_holdback_matches_stateless_scan_per_chunk)。

TableHoldbackScanner::reset70–72 ↗
fn reset(&mut self)

作用:把扫描器清空,恢复到刚创建时的样子。适合一段流结束、换新内容,或者需要丢掉旧判断时使用。

数据流:进去是一个已经用过的扫描器 → 它调用新建逻辑,覆盖掉原来的偏移量、代码块状态、上一行记录和表格判断 → 出来还是同一个扫描器变量,但内容像新的一样。

调用关系:它站在生命周期重置的位置,不自己逐项清理,而是把活交给 TableHoldbackScanner::new。这样初始化和重置保持一致,不容易漏掉某个状态。

调用图:被 1 处调用(reset);外部调用 1 个(new)。

TableHoldbackScanner::state81–89 ↗
fn state(&self) -> TableHoldbackState

作用:告诉外面当前应该怎么延后表格内容。结果可能是没有表格、疑似表头、或者已经确认表格。

数据流:进去读取扫描器内部保存的两个位置:已确认表格开头、待确认表头开头 → 它优先返回“已确认”,其次返回“疑似表头”,都没有就返回“无” → 出来一个 TableHoldbackState,供界面决定哪些行可以固定、哪些行还要保持可改。

调用关系:它是外层询问结果的窗口。active_tail_budget_lines 会用它来计算活跃尾巴要留多少;push_source_chunk 扫描新文本后也会在日志里记录这个状态。

调用图:被 1 处调用(active_tail_budget_lines)。

TableHoldbackScanner::push_source_chunk98–116 ↗
fn push_source_chunk(&mut self, source_chunk: &str)

作用:把新确认可以扫描的一小段原始文本喂给扫描器。它通常只接收已经完整到换行的内容,避免把半行误判成表格结构。

数据流:进去是一段追加来的 source_chunk → 如果为空就不做事;否则按包含换行符的行逐行拆开,逐行交给 push_line,同时统计字节数、行数和耗时 → 出来没有直接返回值,但扫描器内部的表格状态和源文本偏移量会前进,并写一条跟踪日志。

调用关系:它是流式更新进入本文件的主要入口。push_delta 在收到新输出时会调用它,finalize_remaining 在收尾时也会调用它;它自己不判断每一行细节,而是把每行交给 TableHoldbackScanner::push_line。

调用图:调用 1 个内部函数(push_line);被 2 处调用(finalize_remaining, push_delta);外部调用 2 个(now, trace!)。

TableHoldbackScanner::push_line119–159 ↗
fn push_line(&mut self, source_line: &str)

作用:扫描一行已经提交的源文本,更新“上一行是不是表头”“这一行是不是分隔线”“表格是否确认”等核心判断。

数据流:进去是一行文本,可能带换行符 → 它记录这一行在原始文本里的起点,查看当前是否在代码块中;如果不是普通代码块,就清理引用符号并检查是否像表格行。若上一行是表头、这一行是分隔线,就把表格确认为从上一行开始;若只是看到新的疑似表头,就暂时记住它。最后更新上一行记录、推进代码块追踪器、增加源文本偏移量 → 出来不返回值,但扫描器的状态被推进一行。

调用关系:它是实际状态机,也就是这台小机器的齿轮。push_source_chunk 会反复调用它;它会调用 table_candidate_text 找出可疑表格文本,并调用 FenceTracker 的 kind 和 advance 来判断和更新代码块位置。

调用图:调用 3 个内部函数(table_candidate_text, advance, kind);被 1 处调用(push_source_chunk)。

table_candidate_text167–170 ↗
fn table_candidate_text(line: &str) -> Option<&str>

作用:从一行文字里取出可能用于判断表格的部分。它会先去掉引用块前缀,再确认这行确实有 Markdown 表格那种竖线分段。

数据流:进去是一行文本 → 它先用 strip_blockquote_prefix 去掉类似“>”的引用标记,再裁掉两边空白,然后用 parse_table_segments 看它能不能按表格竖线拆成段 → 如果像表格行,就输出清理后的文本;否则输出 None,表示别拿它当表格候选。

调用关系:它是表格识别前的过滤器。push_line 用它处理实时流里的每一行;测试用的 table_holdback_state 也用它做同样判断,保证测试扫描和增量扫描看待表格的标准一致。

调用图:调用 2 个内部函数(parse_table_segments, strip_blockquote_prefix);被 2 处调用(push_line, table_holdback_state)。

parse_lines_with_fence_state182–201 ↗
fn parse_lines_with_fence_state(source: &str) -> Vec<ParsedLine<'_>>

作用:这是测试用的辅助函数,用来把一整段源文本拆成行,并给每一行标上当时是否处在代码块里。

数据流:进去是一整段 source 文本 → 它创建 FenceTracker,从头按换行拆行;每遇到一行,就记录行文本、当前代码块类型、这一行在原文里的起始位置,然后让追踪器读完这一行并更新状态 → 出来一个 ParsedLine 列表,供测试版扫描器逐行检查。

调用关系:它只在测试编译时存在。table_holdback_state 会先调用它,把原始文本变成带上下文的行列表;它内部依赖 FenceTracker::new 和 advance 维持代码块状态。

调用图:调用 1 个内部函数(new);被 1 处调用(table_holdback_state);外部调用 1 个(new)。

table_holdback_state206–242 ↗
fn table_holdback_state(source: &str) -> TableHoldbackState

作用:这是测试用的“一次性扫描”版本:给它完整文本,它直接算出表格延后状态。它用来和真正的增量扫描器对照,确认逐块扫描不会扫错。

数据流:进去是一整段 source → 它先用 parse_lines_with_fence_state 得到每行及其代码块环境;再两行两行查看是否有“表头行 + 分隔线行”,并跳过普通代码块里的内容。找到就返回 Confirmed 和表格起点;如果没找到,就看最后一个非空行是否像表头,像的话返回 PendingHeader;否则返回 None → 出来是一个 TableHoldbackState,不改动外部状态。

调用关系:它是测试里的参照答案生成器。它调用 table_candidate_text、is_table_header_line 和 is_table_delimiter_line 来复用正式逻辑里的判断标准,帮助测试证明 TableHoldbackScanner 的增量结果和完整扫描结果一致。

调用图:调用 4 个内部函数(parse_lines_with_fence_state, table_candidate_text, is_table_delimiter_line, is_table_header_line)。

tui/src/markdown_render/table_key_value.rs源码 ↗
domain_logicmarkdown table rendering

普通表格适合列少、内容短的情况;但在终端里,如果列很多、文字很长,表格会被压得很窄,单词被拆开,长段文字变成很高很难扫读的“细条”。这个文件就是为这种情况准备的备用排版方案。它先判断一个表格是不是已经不适合继续按网格显示:比如太多单元格里的词比列宽还长,或者长文本列被挤得换了很多行。需要切换时,它会把每一行表格变成一组竖排字段:左边是表头,右边是对应内容;如果宽度够,就左右对齐显示;如果宽度不够,就先显示字段名,再缩进显示字段值。它还会保留终端超链接的位置。也就是说,文字被换行、前面加了缩进以后,链接依然能点到正确的内容。

函数细节9
should_render_records31–71 ↗
fn should_render_records(
    rows: &[Vec<TableCell>],
    column_widths: &[usize],
    metrics: &[TableColumnMetrics],
) -> bool

作用:判断一个表格是否应该放弃传统网格样式,改用竖排的“记录”样式。有人会在真正渲染表格前调用它,避免把已经挤坏的表格继续画成难读的网格。

数据流:输入是一批表格行、每列当前分到的宽度、以及每列内容类型的统计信息。它逐行检查:有没有词比列宽还长,或者有没有长文本列被压得换了很多行。最后它数一数受影响的行够不够多;如果够多,就输出 true,表示该换成竖排记录;否则输出 false,不改动原数据。

调用关系:它由 render_table_lines 在决定表格怎么画时调用。它相当于排版前的“体检员”:如果发现网格显示已经不健康,就建议后续交给 render_records 来画竖排版本;检查长文本是否被严重挤压时,会用到 expansive_cells_are_starved 这个内部判断。

调用图:被 1 处调用(render_table_lines)。

expansive_cells_are_starved73–96 ↗
fn expansive_cells_are_starved(
    row: &[TableCell],
    column_widths: &[usize],
    metrics: &[TableColumnMetrics],
) -> bool

作用:检查一行里那些本来需要较大空间的单元格,是否被列宽挤得太厉害。这里的“expansive”可以理解为内容比较展开的列,比如长句子或大量文字,不适合塞进很窄的小格子。

数据流:输入是一行单元格、各列宽度、各列类型。它只关注非紧凑列,把每个单元格按当前宽度试着换行,然后看换出来的行数是否过多。若一行里有多个展开型单元格都变得很高,或者叙述型文字在很窄的宽度下变成灾难性多行,它就返回 true;否则返回 false。

调用关系:它在 should_render_records 的判断过程中充当更细的检查项。should_render_records 先看有没有被切碎的长词,再靠它判断长文本列是否已经被挤成难读的竖条。它自己主要做遍历和估算,不负责最终渲染。

调用图:外部调用 1 个(iter)。

render_records98–149 ↗
fn render_records(
    headers: &[TableCell],
    rows: &[Vec<TableCell>],
    metrics: &[TableColumnMetrics],
    available_width: Option<usize>,
    label_style: Style,
    separator_style: Style,
)

作用:把整张表格真正画成竖排记录样式。它把每个表头当作字段名,把每个单元格当作字段值,让窄屏下的表格仍然能一项一项读清楚。

数据流:输入是表头、数据行、列类型统计、可用宽度,以及字段名和分隔线的样式。它先算字段名最长有多宽,再判断屏幕是否足够把字段名和值左右对齐。如果够宽,就逐项调用 render_aligned_field;如果不够,就调用 render_stacked_field。每两条记录之间,它会加一条横向分隔线。输出是一组带样式、也可能带超链接信息的终端文本行。

调用关系:它由 render_table_lines 在决定使用竖排表格时调用,是这个文件里的主渲染入口。它不亲自处理每个字段的细节,而是根据宽度把工作分给 render_aligned_field 或 render_stacked_field;最后还会借助 widest_line_width 在没有固定宽度时决定分隔线该画多长。

调用图:调用 3 个内部函数(render_aligned_field, render_stacked_field, new);被 1 处调用(render_table_lines);外部调用 5 个(from, styled, new, iter, iter)。

render_aligned_field151–178 ↗
fn render_aligned_field(
    out: &mut Vec<HyperlinkLine>,
    header: &TableCell,
    value: &TableCell,
    label_width: usize,
    available_width: Option<usize>,
    label_style: Style,
)

作用:在空间比较够的时候,把一个字段画成“字段名在左、字段值在右”的对齐形式。这样读起来像整齐的清单,字段名上下对齐,值也从同一列开始。

数据流:输入是输出列表、一个表头、一个值、字段名统一宽度、可用总宽度和字段名样式。它先算出字段值应该从第几列开始,再把值按剩余宽度换行。第一行会放缩进、字段名和间隔;后续换行只放同样宽度的空白,让值继续对齐。最终它把生成的行追加到输出列表里。

调用关系:它由 render_records 在判断“宽度够对齐显示”时调用。它会调用 wrap_cell 给字段值换行,并把每一行交给 push_prefixed_value_line;后者负责把前缀空格和内容拼起来,同时修正超链接位置。

调用图:调用 3 个内部函数(plain_text, push_prefixed_value_line, wrap_cell);被 1 处调用(render_records);外部调用 3 个(raw, styled, new)。

render_stacked_field180–212 ↗
fn render_stacked_field(
    out: &mut Vec<HyperlinkLine>,
    header: &TableCell,
    value: &TableCell,
    available_width: Option<usize>,
    label_style: Style,
)

作用:在空间太窄时,把一个字段画成上下两段:先显示字段名,再缩进显示字段值。这样虽然不如左右对齐紧凑,但不会把内容硬挤到看不懂。

数据流:输入是输出列表、表头、值、可用宽度和字段名样式。它先按可用宽度把字段名自己换行,并在前面加一点缩进;然后再把字段值按更窄的宽度换行,并用更深的缩进显示。它把这些生成的行依次追加到输出列表里。

调用关系:它由 render_records 在宽度不足以左右对齐时调用。字段名换行时会用 word_wrap_line 和 push_owned_lines,字段值换行时会用 wrap_cell;每一行值最后交给 push_prefixed_value_line 来拼接缩进并维护链接位置。

调用图:调用 7 个内部函数(plain_text, push_prefixed_value_line, wrap_cell, push_owned_lines, new, new, word_wrap_line);被 1 处调用(render_records);外部调用 4 个(from, styled, new, vec!)。

push_prefixed_value_line214–232 ↗
fn push_prefixed_value_line(
    out: &mut Vec<HyperlinkLine>,
    mut prefix: Vec<Span<'static>>,
    mut value_line: HyperlinkLine,
)

作用:把一段前缀,比如空格缩进或字段名,占位加到一行值内容前面。它的重要点是:加了前缀以后,超链接所在的列也要整体往后挪,否则链接会点错地方。

数据流:输入是输出列表、一组前缀文本片段,以及一行已经带有内容和超链接信息的值。它先算前缀总共占几列宽,再把前缀和原来的值内容拼成一行。然后它把原有每个超链接的起止列都加上这个偏移量。最后把修正后的新行追加到输出列表。

调用关系:它被 render_aligned_field 和 render_stacked_field 共同使用,是两种排版方式共享的小工具。上游负责决定前面要加什么;它负责安全地拼起来,并保证终端超链接不会因为缩进或标签而错位。

调用图:调用 1 个内部函数(new);被 2 处调用(render_aligned_field, render_stacked_field);外部调用 1 个(from)。

wrap_cell234–255 ↗
fn wrap_cell(cell: &TableCell, width: usize) -> Vec<HyperlinkLine>

作用:把一个表格单元格按指定宽度换成多行,同时尽量保留原来的样式和超链接。简单说,它就是给单元格内容做“自动换行”的地方。

数据流:输入是一个单元格和目标宽度。如果单元格没有内容,它会返回一行空行。否则它会逐条处理单元格里的原始行,用 word_wrap_line 按宽度切成多行,再把这些新行转换成可长期保存的文本,并用 remap_wrapped_line 重新对应原来的超链接范围。输出是换好行的一组 HyperlinkLine;如果结果为空,也会补一行空行。

调用关系:它被 render_aligned_field、render_stacked_field 用来处理字段值,也被 expansive_cells_are_starved 用来估算某个单元格在当前列宽下会变多高。它把底层换行函数和超链接重映射函数串起来,让上层不必关心这些细节。

调用图:调用 4 个内部函数(new, remap_wrapped_line, new, word_wrap_line);被 2 处调用(render_aligned_field, render_stacked_field);外部调用 3 个(default, new, vec!)。

cell_width257–269 ↗
fn cell_width(cell: &TableCell) -> usize

作用:计算一个单元格原始内容中最宽的一行有多宽。这个宽度常用于没有外部宽度限制时,给内容一个合适的显示宽度。

数据流:输入是一个单元格。它查看单元格里的每一行,把这一行里所有文字片段的显示宽度加起来,再从所有行里取最大值。输出是这个最大宽度;如果单元格没有行,就输出 0。它不修改任何内容。

调用关系:它被渲染函数用作宽度估算,特别是在没有给定 available_width 时,render_aligned_field 和 render_stacked_field 需要靠它知道值大概应该占多宽。它是一个安静的测量工具,只负责量尺寸。

widest_line_width271–283 ↗
fn widest_line_width(lines: &[HyperlinkLine]) -> usize

作用:计算一组已经渲染好的终端文本行里,最宽的一行有多宽。它主要用来决定分隔线应该画多长。

数据流:输入是一组 HyperlinkLine。它逐行把这一行所有文字片段的显示宽度加起来,然后取最大值。输出是最宽行的宽度;如果没有任何行,就输出 0。它不改变这些行本身。

调用关系:它在 render_records 里用于没有固定可用宽度的情况。当两条竖排记录之间需要画分隔线时,render_records 会请它量一下当前已经生成的内容有多宽,再按这个宽度生成分隔线。

调用图:外部调用 1 个(iter)。

tui/src/render/highlight.rs源码 ↗
domain_logiccross-cutting

终端里显示代码时,如果所有文字都一个颜色,读起来很费劲。这个文件就像一个“彩色笔盒”:先用 syntect(一个识别代码语言并给不同部分配颜色的库)判断哪些是关键字、字符串、数字,再把颜色转换成 ratatui(这个项目用来画终端界面的库)能理解的样式。它内置很多语言和 32 个主题,也允许用户把自己的 .tmTheme 主题文件放到配置目录里。主题用全局单例保存,意思是全程序共享一份,启动时定好,运行中也能临时切换做预览。它还很谨慎:超过 512KB 或 10000 行的内容直接不高亮,让调用方显示普通文本,防止一次大文件把界面拖慢。

函数细节80
syntax_set59–61 ↗
fn syntax_set() -> &'static SyntaxSet

作用:拿到全程序共用的“语言规则库”。这个库知道 Rust、Python、Markdown 等代码该怎么拆成不同语法部分。

数据流:没有外部输入 → 如果规则库还没建,就从 two_face 初始化一份;如果已经有了,就直接复用 → 返回同一份只读的语法规则库,不改别的数据。

调用关系:find_syntax 用它查某个语言名能不能识别;highlight_to_line_spans_with_theme 在真正高亮每一行时也用它给 syntect 提供规则。

调用图:被 2 处调用(find_syntax, highlight_to_line_spans_with_theme)。

set_theme_override81–101 ↗
fn set_theme_override(
    name: Option<String>,
    codex_home: Option<PathBuf>,
) -> Option<String>

作用:在启动时记住用户配置的主题名和配置目录,并尽量立刻应用这个主题。它还能返回给用户看的警告,比如主题名写错了。

数据流:输入一个可选主题名和可选配置目录 → 先检查主题是否存在,再把这两个设置写进只能初始化一次的全局槽;如果当前主题已经初始化,还会马上切换运行中的主题 → 输出可能的警告文字,并可能改变当前主题。

调用关系:run_ratatui_app 在应用启动、配置最终确定后调用它;它内部会请 validate_theme_name 检查名字,请 resolve_theme_with_override 找到实际主题,必要时交给 set_syntax_theme 切换。

调用图:调用 3 个内部函数(resolve_theme_with_override, set_syntax_theme, validate_theme_name);被 1 处调用(run_ratatui_app);外部调用 1 个(debug!)。

validate_theme_name105–133 ↗
fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>) -> Option<String>

作用:检查用户写的主题名是不是可用。可用就安静返回,不可用就生成一段容易理解的提示。

数据流:输入可选主题名和配置目录 → 没写主题就不管;先看是不是内置主题,再看配置目录里有没有同名 .tmTheme 文件并且能不能读 → 输出 None 表示没问题,或输出一段警告文字。

调用关系:set_theme_override 启动时用它提前发现配置错误;测试也专门用它确认缺文件、坏文件、内置主题等情况的提示是否正确。

调用图:调用 3 个内部函数(custom_theme_path, load_custom_theme, parse_theme_name);被 3 处调用(set_theme_override, validate_theme_name_warns_for_missing_custom, validate_theme_name_warns_when_custom_file_is_invalid);外部调用 1 个(format!)。

parse_theme_name136–172 ↗
fn parse_theme_name(name: &str) -> Option<EmbeddedThemeName>

作用:把用户写的短横线风格主题名,比如 dracula,翻译成程序内部的内置主题编号。

数据流:输入一个字符串主题名 → 在固定名单里逐个匹配 → 匹配到就返回对应内置主题,匹配不到就返回 None。

调用关系:配置检查、主题解析、当前配置主题名判断都会用它;它是内置主题名字和 two_face 内部枚举之间的翻译表。

调用图:被 4 处调用(configured_theme_name, resolve_theme_by_name, resolve_theme_with_override, validate_theme_name)。

custom_theme_path175–177 ↗
fn custom_theme_path(name: &str, codex_home: &Path) -> PathBuf

作用:拼出自定义主题文件应该放在哪里。规则是配置目录下面的 themes/{名字}.tmTheme。

数据流:输入主题名和 codex_home 路径 → 拼接 themes 子目录和文件名 → 返回完整文件路径。

调用关系:load_custom_theme 用它去读文件;validate_theme_name 用它生成提示里的路径,让用户知道该把文件放到哪。

调用图:被 2 处调用(load_custom_theme, validate_theme_name);外部调用 2 个(join, format!)。

load_custom_theme180–182 ↗
fn load_custom_theme(name: &str, codex_home: &Path) -> Option<Theme>

作用:尝试从磁盘读取用户自定义的 .tmTheme 主题文件。

数据流:输入主题名和配置目录 → 先用 custom_theme_path 找到文件,再交给 syntect 的 ThemeSet 解析 → 成功返回主题对象,失败返回 None。

调用关系:主题解析和配置检查都会用它;测试用它确认有效文件能加载、缺失文件不会误判成功。

调用图:调用 1 个内部函数(custom_theme_path);被 5 处调用(configured_theme_name, resolve_theme_by_name, resolve_theme_with_override, load_custom_theme_from_tmtheme_file, validate_theme_name);外部调用 1 个(get_theme)。

adaptive_default_theme_selection184–191 ↗
fn adaptive_default_theme_selection() -> (EmbeddedThemeName, &'static str)

作用:自动选择默认主题:终端背景亮就用浅色主题,背景暗就用深色主题。

数据流:读取当前终端默认背景色 → 判断背景是不是偏亮 → 返回内置主题编号和对应的配置名。

调用关系:adaptive_default_embedded_theme_name 和 adaptive_default_theme_name 都从它拿结果;这样默认主题既能用于真正加载,也能用于界面显示名字。

调用图:调用 2 个内部函数(is_light, default_bg);被 2 处调用(adaptive_default_embedded_theme_name, adaptive_default_theme_name)。

adaptive_default_embedded_theme_name193–195 ↗
fn adaptive_default_embedded_theme_name() -> EmbeddedThemeName

作用:拿到自动默认主题的内部编号,用来真正加载主题。

数据流:没有输入 → 调用 adaptive_default_theme_selection → 返回内置主题枚举值。

调用关系:resolve_theme_with_override 在用户没有有效主题时调用它,作为兜底选择。

调用图:调用 1 个内部函数(adaptive_default_theme_selection);被 1 处调用(resolve_theme_with_override)。

adaptive_default_theme_name199–201 ↗
fn adaptive_default_theme_name() -> &'static str

作用:拿到自动默认主题的文字名字,比如 catppuccin-mocha 或 catppuccin-latte。

数据流:没有输入 → 调用 adaptive_default_theme_selection → 返回短横线风格的主题名字符串。

调用关系:configured_theme_name 用它告诉界面当前配置最终会落到哪个默认主题;恢复运行时主题的流程也会用它。

调用图:调用 1 个内部函数(adaptive_default_theme_selection);被 2 处调用(restore_runtime_theme_from_config, configured_theme_name)。

resolve_theme_with_override205–224 ↗
fn resolve_theme_with_override(name: Option<&str>, codex_home: Option<&Path>) -> Theme

作用:根据用户配置和默认规则,真正选出要用的主题对象。

数据流:输入可选主题名和配置目录 → 先试内置主题,再试自定义 .tmTheme 文件;都不行就记录调试信息并选自动默认主题 → 返回一个可直接用于高亮的 Theme。

调用关系:build_default_theme 初始化全局主题时用它;set_theme_override 在启动或预览时也用它把配置转成实际主题。

调用图:调用 3 个内部函数(adaptive_default_embedded_theme_name, load_custom_theme, parse_theme_name);被 2 处调用(build_default_theme, set_theme_override);外部调用 2 个(debug!, extra)。

build_default_theme228–234 ↗
fn build_default_theme() -> Theme

作用:根据已经保存的全局配置,构建当前默认主题。

数据流:读取 THEME_OVERRIDE 和 CODEX_HOME 里的值 → 交给 resolve_theme_with_override 解析 → 返回主题对象。

调用关系:theme_lock 第一次创建全局主题锁时会调用它;它把启动时保存的配置接到运行时主题系统里。

调用图:调用 1 个内部函数(resolve_theme_with_override)。

theme_lock236–238 ↗
fn theme_lock() -> &'static RwLock<Theme>

作用:拿到保护当前主题的读写锁。读写锁就是一把允许多人读、但写时独占的锁,避免主题切换和高亮同时把数据弄乱。

数据流:没有输入 → 如果全局主题锁还没创建,就用 build_default_theme 创建;否则直接复用 → 返回全局 RwLock<Theme>。

调用关系:current_syntax_theme 读它,set_syntax_theme 写它,highlight_to_line_spans 在高亮时也读它。

调用图:被 3 处调用(current_syntax_theme, highlight_to_line_spans, set_syntax_theme)。

set_syntax_theme241–247 ↗
fn set_syntax_theme(theme: Theme)

作用:运行中切换当前语法高亮主题,主要用于主题选择器的即时预览。

数据流:输入一个 Theme → 获取主题写锁;如果锁曾因异常被污染,也尽量取回里面的数据 → 用新主题替换旧主题,没有返回值。

调用关系:set_theme_override、事件处理和恢复配置主题的流程都会调用它;它是实际改变全局主题的地方。

调用图:调用 1 个内部函数(theme_lock);被 3 处调用(restore_runtime_theme_from_config, handle_event, set_theme_override)。

current_syntax_theme250–255 ↗
fn current_syntax_theme() -> Theme

作用:复制一份当前正在使用的语法主题,方便别人查看或临时保存。

数据流:没有输入 → 获取主题读锁;如果锁被污染也继续读取 → 返回当前 Theme 的克隆,不改全局主题。

调用关系:diff_scope_background_rgbs 和 foreground_style_for_scopes 用它读取颜色;主题选择器也用它保存当前状态,方便取消时恢复。

调用图:调用 1 个内部函数(theme_lock);被 3 处调用(diff_scope_background_rgbs, foreground_style_for_scopes, build_theme_picker_params)。

diff_scope_background_rgbs278–281 ↗
fn diff_scope_background_rgbs() -> DiffScopeBackgroundRgbs

作用:从当前主题里找 diff 增加行和删除行的背景色。diff 就是代码改动对比。

数据流:没有输入 → 复制当前主题 → 交给 diff_scope_background_rgbs_for_theme 分析 → 返回插入和删除背景色,可能都没有。

调用关系:resolve_diff_backgrounds 调用它来决定改动预览的底色;它把全局主题读取和纯粹颜色提取分开。

调用图:调用 2 个内部函数(current_syntax_theme, diff_scope_background_rgbs_for_theme);被 1 处调用(resolve_diff_backgrounds)。

diff_scope_background_rgbs_for_theme285–292 ↗
fn diff_scope_background_rgbs_for_theme(theme: &Theme) -> DiffScopeBackgroundRgbs

作用:从指定主题里提取“新增”和“删除”两类代码范围的背景色。

数据流:输入一个主题 → 建立 highlighter(按主题查样式的小工具),优先查 markup.inserted/deleted,再查 diff.inserted/deleted → 返回两个可选 RGB 颜色。

调用关系:diff_scope_background_rgbs 用它处理当前主题;多组测试用它验证内置主题、自定义主题、回退规则都正常。

调用图:调用 1 个内部函数(scope_background_rgb);被 5 处调用(diff_scope_background_rgbs, bundled_theme_can_provide_diff_scope_backgrounds, custom_tmtheme_diff_scope_backgrounds_are_resolved, diff_scope_backgrounds_prefer_markup_scope_then_diff_fallback, diff_scope_backgrounds_return_none_when_no_background_scope_matches);外部调用 1 个(new)。

scope_background_rgb295–299 ↗
fn scope_background_rgb(highlighter: &Highlighter<'_>, scope_name: &str) -> Option<(u8, u8, u8)>

作用:查询某一个 TextMate scope 的背景色。scope 可以理解为主题里给某类语法区域起的标签。

数据流:输入 highlighter 和 scope 名 → 先把名字解析成 Scope,再问主题这个标签有没有背景色 → 有就返回 RGB 三元组,没有或解析失败就返回 None。

调用关系:diff_scope_background_rgbs_for_theme 用它分别查询新增、删除相关标签。

调用图:被 1 处调用(diff_scope_background_rgbs_for_theme);外部调用 2 个(style_mod_for_stack, new)。

foreground_style_for_scopes303–306 ↗
fn foreground_style_for_scopes(scope_names: &[&str]) -> Option<Style>

作用:从当前主题里找一组 scope 中第一个可用的前景色,并转成终端样式。

数据流:输入多个 scope 名 → 读取当前主题 → 交给 foreground_style_for_scopes_with_theme 查找 → 返回可选的 ratatui Style。

调用关系:表格、标签、数字和活动状态等界面组件会调用它,让普通 UI 颜色也能跟语法主题协调。

调用图:调用 2 个内部函数(current_syntax_theme, foreground_style_for_scopes_with_theme);被 4 处调用(label_style, numeric_style, theme_activity_style, render_table_lines)。

foreground_style_for_scopes_with_theme308–315 ↗
fn foreground_style_for_scopes_with_theme(theme: &Theme, scope_names: &[&str]) -> Option<Style>

作用:在指定主题里按顺序查找 scope 的文字颜色。

数据流:输入主题和 scope 名列表 → 逐个解析 scope,并查询主题有没有前景色;找到第一个就转换成 ratatui 样式 → 返回样式或 None。

调用关系:foreground_style_for_scopes 用它处理当前主题;测试用它确认能读取匹配 scope,也会按输入顺序选择第一个有颜色的 scope。

调用图:被 3 处调用(foreground_style_for_scopes, foreground_style_for_scopes_reads_matching_theme_scope, foreground_style_for_scopes_uses_first_scope_with_foreground);外部调用 1 个(new)。

configured_theme_name322–335 ↗
fn configured_theme_name() -> String

作用:告诉界面“配置上最终算哪个主题”。它看的是持久配置,不看临时预览切换。

数据流:读取全局保存的主题名和配置目录 → 如果用户配置的内置或自定义主题有效,就返回它;否则返回自动默认主题名 → 输出一个字符串。

调用关系:主题选择器参数构建会用它决定当前选中项;相关测试确保坏配置会回到默认显示。

调用图:调用 3 个内部函数(adaptive_default_theme_name, load_custom_theme, parse_theme_name);被 2 处调用(build_theme_picker_params, unavailable_configured_theme_falls_back_to_configured_or_default_selection)。

resolve_theme_by_name339–352 ↗
fn resolve_theme_by_name(name: &str, codex_home: Option<&Path>) -> Option<Theme>

作用:按名字查找主题,既支持内置主题,也支持配置目录里的自定义主题。

数据流:输入主题名和可选配置目录 → 先按内置主题解析,再尝试加载自定义 .tmTheme → 成功返回 Theme,失败返回 None。

调用关系:主题选择器事件和从配置恢复主题都会用它;测试也用它拿到指定主题来检查颜色行为。

调用图:调用 2 个内部函数(load_custom_theme, parse_theme_name);被 6 处调用(restore_runtime_theme_from_config, handle_event, ansi_family_themes_use_terminal_palette_colors_not_rgb, bundled_theme_can_provide_diff_scope_backgrounds, custom_tmtheme_diff_scope_backgrounds_are_resolved, unique_foreground_colors_for_theme);外部调用 1 个(extra)。

list_available_themes366–402 ↗
fn list_available_themes(codex_home: Option<&Path>) -> Vec<ThemeEntry>

作用:列出主题选择器里能选的所有主题,包括内置主题和有效的自定义主题。

数据流:输入可选配置目录 → 先放入全部内置主题;再扫描 themes 目录,只加入能成功解析且不重名的 .tmTheme 文件 → 返回按名字稳定排序的 ThemeEntry 列表。

调用关系:build_theme_picker_params 调用它生成主题选择界面;测试确认坏主题文件不会出现、不同文件系统下排序也稳定。

调用图:被 3 处调用(list_available_themes_excludes_invalid_custom_files, list_available_themes_returns_stable_sorted_order, build_theme_picker_params);外部调用 2 个(get_theme, read_dir)。

ansi_palette_color452–465 ↗
fn ansi_palette_color(index: u8) -> RtColor

作用:把 ANSI 调色板编号变成终端颜色。ANSI 是终端常用的一套基础颜色编号。

数据流:输入 0 到 255 的颜色编号 → 0 到 7 转成 Black、Red 等命名颜色,其他转成 Indexed 编号色 → 返回 ratatui 颜色。

调用关系:convert_syntect_color 在遇到特殊 ANSI 主题编码时调用它,保证终端主题能按终端自己的调色板显示。

调用图:被 1 处调用(convert_syntect_color);外部调用 1 个(Indexed)。

convert_syntect_color481–492 ↗
fn convert_syntect_color(color: SyntectColor) -> Option<RtColor>

作用:把 syntect 的颜色格式转换成 ratatui 的终端颜色格式,并理解一些主题用透明度字段塞进去的特殊含义。

数据流:输入 syntect Color → 如果 alpha 是 0,就把 r 当 ANSI 调色板编号;alpha 是 1,就表示用终端默认色;alpha 是 255 或其他值,就当普通 RGB → 返回可选终端颜色。

调用关系:convert_style 用它转换前景色;这是 ANSI/base16 主题不被错误当成普通 RGB 的关键。

调用图:调用 1 个内部函数(ansi_palette_color);被 1 处调用(convert_style);外部调用 1 个(Rgb)。

convert_style498–517 ↗
fn convert_style(syn_style: SyntectStyle) -> Style

作用:把 syntect 给出的完整样式转换成终端界面能画的样式。

数据流:输入 syntect Style → 转换前景色,故意忽略背景色;如果有粗体就保留,斜体和下划线故意丢掉 → 返回 ratatui Style。

调用关系:highlight_to_line_spans_with_theme 给每段代码文字上样式时调用它;测试覆盖颜色、粗体、斜体和下划线的取舍。

调用图:调用 1 个内部函数(convert_syntect_color);被 7 处调用(highlight_to_line_spans_with_theme, convert_style_suppresses_underline, style_conversion_correctness, style_conversion_unexpected_alpha_falls_back_to_rgb, style_conversion_uses_ansi_named_color_when_alpha_is_zero_low_index, style_conversion_uses_indexed_color_when_alpha_is_zero_high_index, style_conversion_uses_terminal_default_when_alpha_is_one);外部调用 1 个(default)。

find_syntax525–561 ↗
fn find_syntax(lang: &str) -> Option<&'static SyntaxReference>

作用:根据用户传入的语言名或文件扩展名,找到对应的语法规则。

数据流:输入 lang 字符串 → 先修正常见别名,比如 golang 变 go、python3 变 python;再按 token、正式名称、忽略大小写名称、扩展名依次查找 → 返回语法引用或 None。

调用关系:highlight_to_line_spans_with_theme 在高亮前必须先调用它;如果找不到,外层会退回普通文本显示。

调用图:调用 1 个内部函数(syntax_set);被 1 处调用(highlight_to_line_spans_with_theme)。

exceeds_highlight_limits577–579 ↗
fn exceeds_highlight_limits(total_bytes: usize, total_lines: usize) -> bool

作用:快速判断一批内容是否太大,不适合做语法高亮。

数据流:输入总字节数和总行数 → 和 512KB、10000 行两个上限比较 → 返回 true 表示应该跳过高亮。

调用关系:render_change 在处理 diff 改动时用它提前做总量检查,避免逐行高亮时浪费资源。

调用图:被 1 处调用(render_change)。

highlight_to_line_spans_with_theme588–629 ↗
fn highlight_to_line_spans_with_theme(
    code: &str,
    lang: &str,
    theme: &Theme,
) -> Option<Vec<Vec<Span<'static>>>>

作用:这是核心高亮函数:用指定主题把代码拆成一行行带样式的小片段。

数据流:输入代码、语言名和主题 → 空文本、过大文本、未知语言都返回 None;否则逐行让 syntect 标色,去掉换行符和 Windows 的回车符,把每段文字转成 Span → 返回每行的 Span 列表。

调用关系:highlight_to_line_spans 调它处理正式路径;一些测试直接传指定主题进来,避免改全局主题。

调用图:调用 3 个内部函数(convert_style, find_syntax, syntax_set);被 3 处调用(highlight_to_line_spans, ansi_family_themes_use_terminal_palette_colors_not_rgb, unique_foreground_colors_for_theme);外部调用 6 个(new, from, raw, styled, new, new)。

highlight_to_line_spans634–640 ↗
fn highlight_to_line_spans(code: &str, lang: &str) -> Option<Vec<Vec<Span<'static>>>>

作用:用当前全局主题高亮代码,并返回每行的样式片段。

数据流:输入代码和语言名 → 读取当前主题锁 → 调用 highlight_to_line_spans_with_theme → 返回高亮结果或 None。

调用关系:highlight_code_to_lines 和 highlight_code_to_styled_spans 都通过它使用统一的高亮逻辑。

调用图:调用 2 个内部函数(highlight_to_line_spans_with_theme, theme_lock);被 2 处调用(highlight_code_to_lines, highlight_code_to_styled_spans)。

highlight_code_to_lines652–666 ↗
fn highlight_code_to_lines(code: &str, lang: &str) -> Vec<Line<'static>>

作用:给普通代码块生成可以直接渲染的终端 Line。失败时也会返回无样式文本,调用方不用自己兜底。

数据流:输入代码和语言名 → 先尝试高亮;成功就把 Span 组装成 Line;失败就按普通文本逐行生成 Line,空输入也保证有一行 → 返回 Line 列表。

调用关系:Markdown 代码块渲染和 bash 命令显示都用它;highlight_bash_to_lines 只是固定语言为 bash 后转调它。

调用图:调用 1 个内部函数(highlight_to_line_spans);被 9 处调用(end_codeblock, highlight_bash_to_lines, fallback_trailing_newline_no_phantom_line, highlight_crlf_strips_carriage_return, highlight_empty_string, highlight_markdown_preserves_content, highlight_multiline_python, highlight_rust_has_keyword_style, highlight_unknown_lang_falls_back);外部调用 2 个(from, new)。

highlight_bash_to_lines669–671 ↗
fn highlight_bash_to_lines(script: &str) -> Vec<Line<'static>>

作用:专门给 bash 脚本上色的便捷入口。

数据流:输入脚本文本 → 调用 highlight_code_to_lines,并把语言固定为 bash → 返回可渲染的 Line 列表。

调用关系:命令头、命令展示、会话记录等地方用它显示 shell 命令;它避免各处重复写语言名。

调用图:调用 1 个内部函数(highlight_code_to_lines);被 4 处调用(build_header, command_display_lines, transcript_lines, highlight_bash_preserves_content)。

highlight_code_to_styled_spans682–687 ↗
fn highlight_code_to_styled_spans(
    code: &str,
    lang: &str,
) -> Option<Vec<Vec<Span<'static>>>>

作用:给 diff 等更复杂的渲染场景提供按行分好的高亮片段,而不是完整 Line。

数据流:输入代码和语言名 → 调用 highlight_to_line_spans → 成功返回每行 Span,失败返回 None,让调用方改用自己的普通 diff 着色。

调用关系:diff 预览和相关快照测试会调用它;它让 diff 渲染能把语法颜色和增删行颜色组合起来。

调用图:调用 1 个内部函数(highlight_to_line_spans);被 8 处调用(ui_snapshot_syntax_highlighted_insert_wraps, ui_snapshot_syntax_highlighted_insert_wraps_text, update_diff_preserves_multiline_highlight_state_within_hunk, highlight_code_to_styled_spans_returns_some_for_known, highlight_large_input_falls_back, highlight_many_lines_falls_back, highlight_many_lines_no_trailing_newline_falls_back, render_preview)。

tests::write_minimal_tmtheme701–717 ↗
fn write_minimal_tmtheme(path: &Path)

作用:测试辅助函数,写出一个最小但有效的 .tmTheme 文件。

数据流:输入文件路径 → 写入一段简单的 XML plist 主题内容 → 文件系统里多出一个可被 syntect 解析的主题文件。

调用关系:自定义主题加载和主题列表测试用它快速搭建临时主题文件。

调用图:外部调用 1 个(write)。

tests::write_tmtheme_with_diff_backgrounds719–754 ↗
fn write_tmtheme_with_diff_backgrounds(
        path: &Path,
        inserted_scope: &str,
        inserted_background: &str,
        deleted_scope: &str,
        deleted_background: &str,
    )

作用:测试辅助函数,写出带有 diff 背景色设置的自定义主题。

数据流:输入路径、插入/删除 scope 名和颜色字符串 → 拼成 .tmTheme XML 并写入磁盘 → 得到一个可测试 diff 颜色提取的主题文件。

调用关系:custom_tmtheme_diff_scope_backgrounds_are_resolved 用它创建测试材料,再验证解析出的新增/删除颜色正确。

调用图:外部调用 2 个(format!, write)。

tests::reconstructed757–768 ↗
fn reconstructed(lines: &[Line<'static>]) -> String

作用:把高亮后的 Line 重新拼回纯文本,用来确认高亮没有改坏内容。

数据流:输入 Line 列表 → 逐个取出 Span 的文字内容并按行用换行符连接 → 返回重建后的字符串。

调用关系:多个测试用它比较原文和高亮后的文本,确保上色只加样式、不改文字。

调用图:外部调用 1 个(iter)。

tests::unique_foreground_colors_for_theme770–787 ↗
fn unique_foreground_colors_for_theme(theme_name: &str) -> Vec<String>

作用:测试辅助函数,收集某个主题高亮 Rust 示例时用到的所有前景色。

数据流:输入主题名 → 解析主题,调用核心高亮函数高亮一段 Rust 代码,收集每个 Span 的前景色并去重排序 → 返回颜色描述列表。

调用关系:ANSI 调色板快照测试用它观察 ansi、base16 等主题实际产生哪些终端颜色。

调用图:调用 2 个内部函数(highlight_to_line_spans_with_theme, resolve_theme_by_name)。

tests::theme_item789–797 ↗
fn theme_item(scope: &str, background: Option<(u8, u8, u8)>) -> ThemeItem

作用:测试辅助函数,构造一个带背景色的主题规则。

数据流:输入 scope 名和可选 RGB 背景色 → 解析 scope,并填进 syntect 的 ThemeItem 结构 → 返回可放进测试主题的规则。

调用关系:diff 背景色提取测试用它手工搭主题,不需要真实主题文件。

调用图:外部调用 2 个(from_str, default)。

tests::theme_item_with_foreground799–812 ↗
fn theme_item_with_foreground(scope: &str, foreground: (u8, u8, u8)) -> ThemeItem

作用:测试辅助函数,构造一个带前景色的主题规则。

数据流:输入 scope 名和 RGB 前景色 → 解析 scope,并填入 ThemeItem 的 foreground 字段 → 返回测试用主题规则。

调用关系:foreground_style_for_scopes 相关测试用它制造可控的颜色规则。

调用图:外部调用 2 个(from_str, default)。

tests::assert_rgb814–819 ↗
fn assert_rgb(color: Option<RtColor>, expected: (u8, u8, u8))

作用:测试辅助断言,确认一个可选终端颜色正好是指定 RGB。

数据流:输入可选颜色和期望 RGB → 如果不是 RGB 就直接让测试失败;如果是 RGB 就比较三个通道 → 没有返回值,只在不符合时失败。

调用关系:前景色读取测试用它把颜色检查写得更清楚。

调用图:外部调用 2 个(assert_eq!, panic!)。

tests::highlight_rust_has_keyword_style822–835 ↗
fn highlight_rust_has_keyword_style()

作用:测试 Rust 代码能被识别,并且关键字 fn 真的有样式。

数据流:准备一段 Rust 代码 → 调用 highlight_code_to_lines,再重建文本和查找 fn 片段 → 断言内容不变且 fn 不是默认样式。

调用关系:它覆盖从公共入口到语法识别、样式转换的基本成功路径。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 2 个(assert!, assert_eq!)。

tests::highlight_unknown_lang_falls_back838–852 ↗
fn highlight_unknown_lang_falls_back()

作用:测试未知语言不会报错,而是退回普通文本。

数据流:输入随便的文本和不存在的语言名 → 调用 highlight_code_to_lines → 断言文字不变,所有 Span 都没有样式。

调用关系:它验证 find_syntax 找不到语言时,公共接口的兜底行为可靠。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 1 个(assert_eq!)。

tests::fallback_trailing_newline_no_phantom_line855–867 ↗
fn fallback_trailing_newline_no_phantom_line()

作用:测试普通文本兜底时,末尾换行不会多生成一行空白。

数据流:输入以换行结尾的未知语言代码 → 调用 highlight_code_to_lines → 断言只得到一行 hello world。

调用关系:它保护 Markdown 代码块等上游传入尾随换行时的显示效果。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 1 个(assert_eq!)。

tests::highlight_empty_string870–874 ↗
fn highlight_empty_string()

作用:测试空字符串也能产生一行空 Line,而不是完全没有内容。

数据流:输入空代码和 rust 语言 → 调用 highlight_code_to_lines → 断言结果长度为 1,重建文本为空。

调用关系:它确保渲染层收到空代码块时仍有稳定的结构可画。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 1 个(assert_eq!)。

tests::highlight_bash_preserves_content877–881 ↗
fn highlight_bash_preserves_content()

作用:测试 bash 高亮不会改变命令文本。

数据流:输入一段 shell 命令 → 调用 highlight_bash_to_lines,再重建文本 → 断言和原命令完全一致。

调用关系:它覆盖 bash 便捷入口,并间接验证 highlight_code_to_lines 的内容保持能力。

调用图:调用 1 个内部函数(highlight_bash_to_lines);外部调用 1 个(assert_eq!)。

tests::highlight_crlf_strips_carriage_return884–898 ↗
fn highlight_crlf_strips_carriage_return()

作用:测试 Windows 风格换行里的回车符不会残留到显示文本中。

数据流:输入带 \r\n 的 Rust 代码 → 调用 highlight_code_to_lines → 检查每个 Span 内容都不含 \r。

调用关系:它保护 highlight_to_line_spans_with_theme 中去掉行尾换行符的逻辑。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 1 个(assert!)。

tests::style_conversion_correctness902–926 ↗
fn style_conversion_correctness()

作用:测试基本样式转换:RGB 前景色和粗体保留,背景、斜体不保留。

数据流:构造一个 syntect Style → 调用 convert_style → 断言前景色、背景、粗体和斜体状态符合预期。

调用关系:它直接覆盖 convert_style 的设计取舍,防止终端显示变得花或难读。

调用图:调用 1 个内部函数(convert_style);外部调用 2 个(assert!, assert_eq!)。

tests::convert_style_suppresses_underline929–955 ↗
fn convert_style_suppresses_underline()

作用:测试主题里的下划线会被压掉。

数据流:构造一个带下划线的 syntect Style → 调用 convert_style → 断言输出样式没有下划线。

调用关系:它保护 Dracula 等主题下类型名不会在终端里出现大量干扰性下划线。

调用图:调用 1 个内部函数(convert_style);外部调用 1 个(assert!)。

tests::style_conversion_uses_ansi_named_color_when_alpha_is_zero_low_index958–976 ↗
fn style_conversion_uses_ansi_named_color_when_alpha_is_zero_low_index()

作用:测试特殊 ANSI 编码的低编号颜色会转成命名颜色。

数据流:构造 alpha 为 0、r 为 2 的颜色 → 调用 convert_style → 断言前景色是 Green。

调用关系:它验证 convert_syntect_color 和 ansi_palette_color 对 ANSI 主题的兼容。

调用图:调用 1 个内部函数(convert_style);外部调用 2 个(empty, assert_eq!)。

tests::style_conversion_uses_indexed_color_when_alpha_is_zero_high_index979–997 ↗
fn style_conversion_uses_indexed_color_when_alpha_is_zero_high_index()

作用:测试特殊 ANSI 编码的高编号颜色会转成 Indexed 颜色。

数据流:构造 alpha 为 0、r 为 0x9a 的颜色 → 调用 convert_style → 断言输出是 Indexed(0x9a)。

调用关系:它覆盖 8 到 255 号 ANSI 调色板颜色的转换路径。

调用图:调用 1 个内部函数(convert_style);外部调用 2 个(empty, assert!)。

tests::style_conversion_uses_terminal_default_when_alpha_is_one1000–1018 ↗
fn style_conversion_uses_terminal_default_when_alpha_is_one()

作用:测试 alpha 为 1 时表示“用终端默认颜色”。

数据流:构造 alpha 为 1 的前景色 → 调用 convert_style → 断言输出没有设置前景色。

调用关系:它保护 ANSI/base16 主题里“默认色”的特殊约定不被误当成黑色。

调用图:调用 1 个内部函数(convert_style);外部调用 2 个(empty, assert_eq!)。

tests::style_conversion_unexpected_alpha_falls_back_to_rgb1021–1039 ↗
fn style_conversion_unexpected_alpha_falls_back_to_rgb()

作用:测试遇到不常见 alpha 值时仍按普通 RGB 处理。

数据流:构造 alpha 为 0x80 的颜色 → 调用 convert_style → 断言输出 RGB(10,20,30)。

调用关系:它让 convert_syntect_color 对非标准主题保持宽容。

调用图:调用 1 个内部函数(convert_style);外部调用 2 个(empty, assert!)。

tests::ansi_palette_color_maps_ansi_white_to_gray1042–1044 ↗
fn ansi_palette_color_maps_ansi_white_to_gray()

作用:测试 ANSI 里的白色编号 7 在 ratatui 中对应 Gray。

数据流:输入颜色编号 0x07 → 调用 ansi_palette_color → 断言结果是 RtColor::Gray。

调用关系:它固定低编号 ANSI 颜色的映射细节,避免显示行为被无意改掉。

调用图:外部调用 1 个(assert_eq!)。

tests::ansi_family_themes_use_terminal_palette_colors_not_rgb1047–1074 ↗
fn ansi_family_themes_use_terminal_palette_colors_not_rgb()

作用:测试 ansi、base16、base16-256 这些主题不会产生普通 RGB 前景色。

数据流:逐个解析主题并高亮 Rust 示例 → 遍历所有 Span 的前景色 → 如果出现 RGB 就失败,同时确认至少有非默认颜色。

调用关系:它直接保护特殊 alpha 编码的解释逻辑,是防止上游主题格式变化的报警器。

调用图:调用 2 个内部函数(highlight_to_line_spans_with_theme, resolve_theme_by_name);外部调用 2 个(assert!, panic!)。

tests::ansi_family_foreground_palette_snapshot1077–1087 ↗
fn ansi_family_foreground_palette_snapshot()

作用:生成 ANSI 系列主题前景色列表的快照,方便发现颜色输出变化。

数据流:遍历三个 ANSI 系列主题 → 收集去重后的前景色并拼成文本 → 用快照断言比较。

调用关系:它调用 unique_foreground_colors_for_theme,把颜色行为固定成可审查的测试快照。

调用图:外部调用 4 个(new, assert_snapshot!, format!, unique_foreground_colors_for_theme)。

tests::highlight_multiline_python1090–1095 ↗
fn highlight_multiline_python()

作用:测试多行 Python 代码高亮后仍保持三行和原文内容。

数据流:输入三行 Python 代码 → 调用 highlight_code_to_lines → 断言重建文本相同且行数为 3。

调用关系:它覆盖多行输入的正常高亮路径。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 1 个(assert_eq!)。

tests::highlight_code_to_styled_spans_returns_none_for_unknown1098–1100 ↗
fn highlight_code_to_styled_spans_returns_none_for_unknown()

作用:测试给 diff 用的 Span 接口遇到未知语言会返回 None。

数据流:输入代码 x 和不存在的语言名 → 检查 highlight_code_to_styled_spans 的结果 → 断言是 None。

调用关系:它确认 diff 渲染能通过 None 知道该走普通着色兜底。

调用图:外部调用 1 个(assert!)。

tests::highlight_code_to_styled_spans_returns_some_for_known1103–1108 ↗
fn highlight_code_to_styled_spans_returns_some_for_known()

作用:测试给 diff 用的 Span 接口遇到已知语言会返回内容。

数据流:输入 Rust 片段 → 调用 highlight_code_to_styled_spans → 断言结果存在且不为空。

调用关系:它覆盖 diff 高亮接口的成功路径。

调用图:调用 1 个内部函数(highlight_code_to_styled_spans);外部调用 1 个(assert!)。

tests::highlight_markdown_preserves_content1111–1119 ↗
fn highlight_markdown_preserves_content()

作用:测试 Markdown 内容高亮时不会破坏里面的反引号和换行文字。

数据流:输入一段包含围栏代码块文本的 Markdown → 调用 highlight_code_to_lines 并重建 → 断言与原文完全一致。

调用关系:它保护 Markdown 语法高亮只加样式、不重写内容。

调用图:调用 1 个内部函数(highlight_code_to_lines);外部调用 2 个(assert_eq!, reconstructed)。

tests::highlight_large_input_falls_back1122–1128 ↗
fn highlight_large_input_falls_back()

作用:测试超过 512KB 的输入会跳过高亮。

数据流:构造超大字符串 → 调用 highlight_code_to_styled_spans → 断言返回 None。

调用关系:它验证核心保护栏,防止大输入拖慢界面。

调用图:调用 1 个内部函数(highlight_code_to_styled_spans);外部调用 1 个(assert!)。

tests::highlight_many_lines_falls_back1131–1136 ↗
fn highlight_many_lines_falls_back()

作用:测试超过 10000 行的输入会跳过高亮。

数据流:构造行数超限的 Rust 文本 → 调用 highlight_code_to_styled_spans → 断言返回 None。

调用关系:它保护按行数量限制资源消耗的逻辑。

调用图:调用 1 个内部函数(highlight_code_to_styled_spans);外部调用 1 个(assert!)。

tests::highlight_many_lines_no_trailing_newline_falls_back1139–1151 ↗
fn highlight_many_lines_no_trailing_newline_falls_back()

作用:测试没有末尾换行时,真实行数超限也能被发现。

数据流:构造 10001 行但最后一行不带换行的文本 → 先确认 lines() 计数,再调用高亮接口 → 断言返回 None。

调用关系:它防止用换行符数量粗略计数导致漏掉最后一行。

调用图:调用 1 个内部函数(highlight_code_to_styled_spans);外部调用 2 个(assert!, assert_eq!)。

tests::find_syntax_resolves_languages_and_aliases1154–1217 ↗
fn find_syntax_resolves_languages_and_aliases()

作用:测试常见语言、扩展名和手工修补的别名都能被识别。

数据流:遍历多组语言名、扩展名和别名 → 调用 find_syntax → 断言每个都能找到语法规则。

调用关系:它直接守住 find_syntax 的语言覆盖面,尤其是 csharp、golang、python3 等常见别名。

调用图:外部调用 1 个(assert!)。

tests::diff_scope_backgrounds_prefer_markup_scope_then_diff_fallback1220–1237 ↗
fn diff_scope_backgrounds_prefer_markup_scope_then_diff_fallback()

作用:测试 diff 背景色优先用 markup.,没有时才用 diff.

数据流:手工构造一个含 markup.inserted 和 diff.deleted 的主题 → 调用 diff_scope_background_rgbs_for_theme → 断言插入、删除颜色分别来自正确规则。

调用关系:它覆盖 diff_scope_background_rgbs_for_theme 的优先级设计。

调用图:调用 1 个内部函数(diff_scope_background_rgbs_for_theme);外部调用 4 个(default, default, assert_eq!, vec!)。

tests::diff_scope_backgrounds_return_none_when_no_background_scope_matches1240–1254 ↗
fn diff_scope_backgrounds_return_none_when_no_background_scope_matches()

作用:测试主题没有相关 diff 背景色时会返回空结果。

数据流:构造一个只有普通 numeric scope 的主题 → 调用 diff_scope_background_rgbs_for_theme → 断言 inserted 和 deleted 都是 None。

调用关系:它确保 diff 渲染能知道主题没提供颜色,然后使用自己的默认调色。

调用图:调用 1 个内部函数(diff_scope_background_rgbs_for_theme);外部调用 4 个(default, default, assert_eq!, vec!)。

tests::foreground_style_for_scopes_reads_matching_theme_scope1257–1268 ↗
fn foreground_style_for_scopes_reads_matching_theme_scope()

作用:测试能从主题里读到匹配 scope 的前景色。

数据流:构造 keyword 前景色为 (10,20,30) 的主题 → 调用 foreground_style_for_scopes_with_theme → 断言输出 RGB 正确。

调用关系:它覆盖普通 UI 借用语法主题颜色的基础能力。

调用图:调用 1 个内部函数(foreground_style_for_scopes_with_theme);外部调用 4 个(default, default, assert_rgb, vec!)。

tests::foreground_style_for_scopes_uses_first_scope_with_foreground1271–1282 ↗
fn foreground_style_for_scopes_uses_first_scope_with_foreground()

作用:测试多个 scope 里会选择第一个真正有前景色的项。

数据流:构造只有 string 有颜色的主题 → 输入 keyword、string 两个候选 → 断言返回 string 的颜色。

调用关系:它保护 foreground_style_for_scopes_with_theme 的顺序查找规则。

调用图:调用 1 个内部函数(foreground_style_for_scopes_with_theme);外部调用 4 个(default, default, assert_rgb, vec!)。

tests::bundled_theme_can_provide_diff_scope_backgrounds1285–1293 ↗
fn bundled_theme_can_provide_diff_scope_backgrounds()

作用:测试至少某个内置主题能提供 diff 增删背景色。

数据流:加载内置 github 主题 → 调用 diff_scope_background_rgbs_for_theme → 断言插入和删除颜色都存在。

调用关系:它验证内置主题和 diff 颜色提取逻辑能实际配合。

调用图:调用 2 个内部函数(diff_scope_background_rgbs_for_theme, resolve_theme_by_name);外部调用 1 个(assert!)。

tests::custom_tmtheme_diff_scope_backgrounds_are_resolved1296–1318 ↗
fn custom_tmtheme_diff_scope_backgrounds_are_resolved()

作用:测试自定义 .tmTheme 里的 diff 背景色能被正确解析。

数据流:创建临时 themes 目录并写入自定义 diff 主题 → 用 resolve_theme_by_name 加载,再提取背景色 → 断言 RGB 与文件中写的一致。

调用关系:它把自定义主题加载和 diff 颜色提取连起来测试。

调用图:调用 2 个内部函数(diff_scope_background_rgbs_for_theme, resolve_theme_by_name);外部调用 4 个(assert_eq!, create_dir, tempdir, write_tmtheme_with_diff_backgrounds)。

tests::parse_theme_name_covers_all_variants1321–1378 ↗
fn parse_theme_name_covers_all_variants()

作用:测试每个已知内置主题名都能映射到正确的内部主题枚举。

数据流:遍历主题名和期望枚举的清单 → 调用 parse_theme_name → 逐项断言相等。

调用关系:它保护 parse_theme_name 这张手写映射表不被改错。

调用图:外部调用 1 个(assert_eq!)。

tests::parse_theme_name_returns_none_for_unknown1381–1384 ↗
fn parse_theme_name_returns_none_for_unknown()

作用:测试未知主题名和空字符串不会被误认为内置主题。

数据流:输入 nonexistent-theme 和空字符串 → 调用 parse_theme_name → 断言都返回 None。

调用关系:它覆盖主题名解析的失败路径。

调用图:外部调用 1 个(assert_eq!)。

tests::load_custom_theme_from_tmtheme_file1387–1394 ↗
fn load_custom_theme_from_tmtheme_file()

作用:测试有效的自定义 .tmTheme 文件可以被加载。

数据流:创建临时 themes 目录并写入最小主题文件 → 调用 load_custom_theme → 断言返回 Some。

调用关系:它验证 custom_theme_path 和 syntect 主题解析能一起工作。

调用图:调用 1 个内部函数(load_custom_theme);外部调用 4 个(assert!, create_dir, tempdir, write_minimal_tmtheme)。

tests::load_custom_theme_returns_none_for_missing1397–1400 ↗
fn load_custom_theme_returns_none_for_missing()

作用:测试自定义主题文件不存在时加载结果是 None。

数据流:创建空临时目录 → 尝试加载不存在的主题名 → 断言没有主题返回。

调用关系:它保护 load_custom_theme 的失败行为,避免缺文件被当成成功。

调用图:外部调用 2 个(assert!, tempdir)。

tests::validate_theme_name_none_for_bundled1403–1407 ↗
fn validate_theme_name_none_for_bundled()

作用:测试内置主题名不会产生警告。

数据流:输入 dracula、nord 等内置主题名 → 调用 validate_theme_name → 断言返回 None。

调用关系:它确认配置检查不会误吓用户。

调用图:外部调用 1 个(assert!)。

tests::validate_theme_name_none_when_no_override1410–1412 ↗
fn validate_theme_name_none_when_no_override()

作用:测试用户没有配置主题时不会有警告。

数据流:输入 None 作为主题名 → 调用 validate_theme_name → 断言返回 None。

调用关系:它覆盖默认配置路径,保证不写主题也正常。

调用图:外部调用 1 个(assert!)。

tests::validate_theme_name_warns_for_missing_custom1415–1424 ↗
fn validate_theme_name_warns_for_missing_custom()

作用:测试自定义主题文件不存在时会给用户警告。

数据流:创建临时目录但不放主题文件 → 调用 validate_theme_name("my-fancy") → 断言有警告且包含主题名。

调用关系:它保护启动时的可操作提示,告诉用户文件该放哪。

调用图:调用 1 个内部函数(validate_theme_name);外部调用 2 个(assert!, tempdir)。

tests::validate_theme_name_none_when_custom_file_is_valid1427–1436 ↗
fn validate_theme_name_none_when_custom_file_is_valid()

作用:测试有效自定义主题不会产生警告。

数据流:创建临时 themes 目录并写入有效 .tmTheme → 调用 validate_theme_name → 断言返回 None。

调用关系:它确认自定义主题配置的正常路径不会被误报。

调用图:外部调用 4 个(assert!, create_dir, tempdir, write_minimal_tmtheme)。

tests::validate_theme_name_warns_when_custom_file_is_invalid1439–1455 ↗
fn validate_theme_name_warns_when_custom_file_is_invalid()

作用:测试自定义主题文件存在但格式坏时会警告。

数据流:写入一个内容不是合法主题的 .tmTheme 文件 → 调用 validate_theme_name → 断言有警告且说明无法加载。

调用关系:它确保用户能区分“文件缺失”和“文件格式坏”。

调用图:调用 1 个内部函数(validate_theme_name);外部调用 4 个(assert!, create_dir, write, tempdir)。

tests::list_available_themes_excludes_invalid_custom_files1458–1479 ↗
fn list_available_themes_excludes_invalid_custom_files()

作用:测试主题列表只收录有效的自定义主题文件。

数据流:在临时 themes 目录里放一个有效主题和一个坏文件 → 调用 list_available_themes → 断言有效主题出现,坏主题不出现。

调用关系:它保护主题选择器不会展示点了也不能用的主题。

调用图:调用 1 个内部函数(list_available_themes);外部调用 5 个(assert!, create_dir, write, tempdir, write_minimal_tmtheme)。

tests::list_available_themes_returns_stable_sorted_order1482–1503 ↗
fn list_available_themes_returns_stable_sorted_order()

作用:测试主题列表排序稳定,不受文件系统读取顺序影响。

数据流:创建多个大小写不同的自定义主题 → 调用 list_available_themes → 用同样规则排序一份期望值并比较。

调用关系:它保证主题选择器在不同系统上显示顺序一致。

调用图:调用 1 个内部函数(list_available_themes);外部调用 4 个(assert_eq!, create_dir, tempdir, write_minimal_tmtheme)。

tests::parse_theme_name_is_exhaustive1506–1566 ↗
fn parse_theme_name_is_exhaustive()

作用:测试 two_face 提供的所有内置主题都被 parse_theme_name 覆盖。

数据流:读取 two_face 的全部主题枚举 → 确认数量仍是 32,再把手写名字全部映射 → 断言每个上游主题都能找到。

调用关系:它像一道保险:如果依赖库新增主题,测试会提醒维护者更新映射表和内置名单。

调用图:外部调用 3 个(theme_names, assert!, assert_eq!)。

tui/src/markdown_render.rs源码 ↗
domain_logicrequest handling / cross-cutting

这个文件像一个“Markdown 排版师”。输入是一段 Markdown,先用解析器拆成标题、段落、列表、链接、表格等小事件,再由 Writer 一边读事件一边拼出最终要显示的行。它会记住当前是不是在列表、引用、代码块、表格单元格里,并据此加缩进、换行和样式。链接会特殊处理:网页链接保留可点击目标,本地文件链接则更强调显示真实路径。表格是这里较复杂的部分:它会先收集单元格内容,再按屏幕宽度决定列宽、换行方式,必要时退回成普通管道表格或键值记录形式。整体目标不是完整复刻网页 Markdown,而是在终端宽度有限、字体等宽的环境里,把内容排得稳定、可读、可复制。 从这段代码能看出,这个文件很在意“显示出来要像人读的东西”。它会把本地文件链接解析成路径和行号,把 file:// 网址、~/ 开头的家目录路径、Windows 反斜杠路径等统一成稳定的样子;显示时还会尽量把当前工作目录下面的绝对路径缩短,让界面少占空间。后半部分是大量测试,专门确认 Markdown 换行、列表缩进、引用、代码块、表格列宽、长网址和超链接标注都不会乱。可以把它想成一个“排版师”:输入是 Markdown,输出是适合终端宽度的文本;同时它还要记住哪些文字能点击、点了该去哪里。

函数细节135
MarkdownStyles::default105–122 ↗
fn default() -> Self

作用:给 Markdown 各种元素准备一套默认外观,比如标题加粗、代码变青色、链接加下划线。没有它,渲染出来的文字就会很难区分层次。

数据流:进去没有参数 → 它创建一组 Style(样式,表示粗体、斜体、颜色等)→ 出来一个 MarkdownStyles,后面的渲染器拿它给文字上色和加效果。

调用关系:Writer::new 创建渲染器时会调用它,相当于开工前先把画笔颜色和字体效果准备好。

调用图:被 1 处调用(new);外部调用 1 个(new)。

IndentContext::new133–139 ↗
fn new(prefix: Vec<Span<'static>>, marker: Option<Vec<Span<'static>>>, is_list: bool) -> Self

作用:记录当前这层缩进该怎么显示,比如引用前面的“> ”、列表项前面的“- ”或编号。它让嵌套列表和引用不会乱套。

数据流:进去前缀、可选列表标记、是否是列表 → 它原样装进一个 IndentContext → 出来一个缩进上下文,后面生成每一行前缀时使用。

调用关系:start_blockquote、start_codeblock、start_item 会调用它,把新进入的引用、代码块或列表项压到缩进栈里。

调用图:被 3 处调用(start_blockquote, start_codeblock, start_item)。

TableCell::ensure_line155–159 ↗
fn ensure_line(&mut self)

作用:保证表格单元格里至少有一行可写。这样追加文字时不会因为单元格还是空的而出错。

数据流:进去一个可修改的单元格 → 如果里面没有行,就放入一条空的 HyperlinkLine(带链接信息的显示行)→ 单元格变成可以继续追加内容的状态。

调用关系:TableCell::push_span 和 TableCell::push_annotated 在真正写内容前会先调用它。

调用图:调用 1 个内部函数(new);被 2 处调用(push_annotated, push_span);外部调用 1 个(default)。

TableCell::push_span162–167 ↗
fn push_span(&mut self, span: Span<'static>)

作用:往表格单元格当前行里追加一小段文字。Span 可以理解成“带样式的一截文字”。

数据流:进去一个 Span → 先确保单元格有行 → 把这段文字加到最后一行末尾,单元格内容被更新。

调用关系:这是表格单元格写普通文字的基础动作,被 Writer 写代码、HTML、软换行等内容时间接使用。

调用图:调用 1 个内部函数(ensure_line)。

TableCell::push_annotated169–180 ↗
fn push_annotated(&mut self, mut appended: HyperlinkLine)

作用:把一整段带超链接标注的内容接到表格单元格当前行后面,并修正链接所在列号。它保证链接拼接后仍然点在正确文字上。

数据流:进去一条 HyperlinkLine → 先确保单元格有行 → 把文字片段接到最后一行,并把新内容里的链接列范围整体右移 → 单元格保留正确的链接位置。

调用关系:Writer::push_text_spans_to_table_cell 会用它把自动识别的网页链接或 Markdown 链接放进单元格。

调用图:调用 1 个内部函数(ensure_line)。

TableCell::hard_break183–185 ↗
fn hard_break(&mut self)

作用:在表格单元格里强制换一行。比如 Markdown 里有硬换行或块级 HTML 时需要这样做。

数据流:进去一个可修改单元格 → 追加一条新的空 HyperlinkLine → 后续内容会写到新行。

调用关系:Writer::push_table_cell_hard_break 会调用它,专门处理表格内部的换行。

调用图:调用 1 个内部函数(new);外部调用 1 个(default)。

TableCell::plain_text187–199 ↗
fn plain_text(&self) -> String

作用:把表格单元格里的样式和链接都去掉,只取可见文字。它主要用于计算表格宽度和判断内容类型。

数据流:进去一个单元格 → 逐行读取所有 Span 的文字,多行之间用空格连接 → 出来一个普通字符串。

调用关系:render_aligned_field、render_stacked_field 以及表格宽度统计逻辑会依赖这种纯文字结果。

调用图:被 2 处调用(render_aligned_field, render_stacked_field);外部调用 2 个(new, write!)。

TableState::new226–236 ↗
fn new(alignments: Vec<Alignment>) -> Self

作用:开始渲染一个表格时,创建一份空的表格临时记录。它会保存表头、行、当前单元格和对齐方式。

数据流:进去每列的对齐设置 → 初始化空表头、空行列表、空当前行和状态标记 → 出来一个可逐步填充的 TableState。

调用关系:Writer::start_table 调用它,之后表格头、行、单元格的开始和结束函数都会往这份状态里填内容。

调用图:被 1 处调用(start_table);外部调用 1 个(new)。

render_markdown_text287–289 ↗
fn render_markdown_text(input: &str) -> Text<'static>

作用:最简单的入口:把 Markdown 字符串渲染成终端可显示的 Text。调用者不用关心宽度和当前目录。

数据流:进去 Markdown 文本 → 转交给 render_markdown_text_with_width,宽度传 None → 出来 ratatui 的 Text(终端 UI 使用的多行文字对象)。

调用关系:测试和普通调用会用它;它只是轻量包装,真正工作交给带宽度的版本。

调用图:调用 1 个内部函数(render_markdown_text_with_width);被 72 处调用(vt100_deep_nested_mixed_list_third_level_marker_is_colored, crlf_code_block_no_extra_blank_lines, fenced_code_info_string_with_metadata_highlights, blockquote_heading_inherits_heading_style, blockquote_in_ordered_list_on_next_line, blockquote_in_unordered_list_on_next_line, blockquote_inside_nested_list, blockquote_list_then_nested_blockquote, blockquote_multiple_with_break, blockquote_nested_two_levels (+15 more))。

render_markdown_text_with_width298–301 ↗
fn render_markdown_text_with_width(input: &str, width: Option<usize>) -> Text<'static>

作用:把 Markdown 渲染成 Text,同时允许指定最大行宽。它还会顺手读取当前目录,用来显示本地文件链接。

数据流:进去文本和可选宽度 → 读取当前工作目录 → 调用 render_markdown_text_with_width_and_cwd → 出来可显示的 Text。

调用关系:render_markdown_text 调用它;需要测试换行效果的地方也直接调用它。

调用图:调用 1 个内部函数(render_markdown_text_with_width_and_cwd);被 20 处调用(render_markdown_text, does_not_split_long_url_like_token_without_scheme, does_not_wrap_code_blocks, wraps_blockquotes, wraps_blockquotes_inside_lists, wraps_list_items_containing_blockquotes, wraps_list_items_preserving_indent, wraps_nested_lists, wraps_ordered_lists, wraps_plain_text_when_width_provided (+10 more));外部调用 1 个(current_dir)。

render_markdown_text_with_width_and_cwd308–316 ↗
fn render_markdown_text_with_width_and_cwd(
    input: &str,
    width: Option<usize>,
    cwd: Option<&Path>,
) -> Text<'static>

作用:把 Markdown 渲染成 Text,并显式指定屏幕宽度和当前目录。当前目录用于把本地链接显示得更友好。

数据流:进去文本、宽度、目录 → 先生成带链接信息的可见行 → 去掉不可见辅助内容并转成 Text → 出来给终端组件显示的结果。

调用关系:append_markdown、append_markdown_agent 和带宽度入口会调用它;底层工作交给 render_markdown_lines_with_width_and_cwd。

调用图:调用 2 个内部函数(render_markdown_lines_with_width_and_cwd, visible_lines);被 8 处调用(append_markdown, append_markdown_agent, render_markdown_text_with_width, consecutive_unordered_list_local_file_links_do_not_detach_paths, render_markdown_text_for_cwd, table_wraps_file_paths_before_collapsing_narrative_columns_snapshot, unordered_list_local_file_link_soft_break_before_colon_stays_inline, unordered_list_local_file_link_stays_inline_with_following_text);外部调用 1 个(from)。

render_markdown_lines_with_width_and_cwd318–330 ↗
fn render_markdown_lines_with_width_and_cwd(
    input: &str,
    width: Option<usize>,
    cwd: Option<&Path>,
) -> Vec<HyperlinkLine>

作用:这是核心渲染入口,输出的不只是文字行,还保留每行里的超链接范围。适合需要点击链接或保留链接注解的界面。

数据流:进去 Markdown、宽度、目录 → 打开删除线和表格支持 → 用 Markdown 解析器生成事件 → 创建 Writer 跑完整个事件流 → 出来 Vec<HyperlinkLine>。

调用关系:上层 Text 渲染和带链接的 agent 渲染都会调用它;它创建 Writer,后续所有具体排版都由 Writer::run 驱动。

调用图:调用 2 个内部函数(new, new);被 10 处调用(render_markdown_agent_with_links_and_cwd, render_markdown_text_with_width_and_cwd, annotates_explicit_web_link_label_and_visible_destination, does_not_annotate_code_or_non_web_markdown_links, key_value_table_keeps_web_annotations, pipe_table_fallback_keeps_web_annotations, wrapped_table_url_fragments_keep_complete_web_destination, bare_url_with_tilde_keeps_complete_hyperlink, merged_text_events_preserve_entity_decoding, table_url_with_tilde_keeps_complete_hyperlink);外部调用 2 个(empty, new_ext)。

Writer::new404–433 ↗
fn new(input: &'a str, iter: I, wrap_width: Option<usize>, cwd: Option<&Path>) -> Self

作用:创建 Markdown 渲染的工作机。它保存输入、解析事件、样式、缩进、链接、代码块、表格等所有中间状态。

数据流:进去原文、事件迭代器、可选换行宽度、可选当前目录 → 初始化一大组状态字段 → 出来一个准备运行的 Writer。

调用关系:render_markdown_lines_with_width_and_cwd 调用它,然后马上调用 Writer::run。

调用图:调用 1 个内部函数(default);被 1 处调用(render_markdown_lines_with_width_and_cwd);外部调用 3 个(new, default, new)。

Writer::run435–440 ↗
fn run(&mut self)

作用:驱动整个渲染过程:不停读取 Markdown 解析事件并处理,最后把没输出的当前行刷出去。

数据流:进去 Writer 自己持有的事件流 → 循环取事件并交给 handle_event → 结束时 flush_current_line → Writer.text 里留下最终行。

调用关系:由 render_markdown_lines_with_width_and_cwd 创建 Writer 后调用;每个事件的细节交给 handle_event。

调用图:调用 2 个内部函数(flush_current_line, handle_event);外部调用 1 个(next)。

Writer::handle_event442–464 ↗
fn handle_event(&mut self, event: Event<'a>, range: Range<usize>)

作用:把解析器给出的一个 Markdown 事件分派给对应处理函数。它是事件到排版动作之间的总路口。

数据流:进去一个 Event 和它在原文中的范围 → 先处理可能挂起的本地链接软换行 → 根据事件类型开始标签、结束标签、写文字、写代码、换行等 → Writer 状态和输出行被更新。

调用关系:Writer::run 每读到一个事件就调用它;它再把工作交给 start_tag、end_tag、text、code、html 等函数。

调用图:调用 11 个内部函数(code, end_tag, flush_current_line, hard_break, html, prepare_for_event, push_blank_line, push_line, soft_break, start_tag (+1 more));被 1 处调用(run);外部调用 1 个(from)。

Writer::prepare_for_event466–481 ↗
fn prepare_for_event(&mut self, event: &Event<'a>)

作用:处理本地文件链接后面紧跟软换行的特殊情况。它避免“文件路径”和后面的说明文字被错误拆成两行。

数据流:进去下一个事件 → 如果没有挂起的本地链接软换行就不动;如果下个文本以冒号开头就保持同行;否则补一条空行 → 更新 pending_local_link_soft_break 状态。

调用关系:handle_event 在处理任何事件前都会调用它,专门补救 soft_break 留下的决定。

调用图:调用 1 个内部函数(push_line);被 1 处调用(handle_event);外部调用 2 个(default, matches!)。

Writer::start_tag483–514 ↗
fn start_tag(&mut self, tag: Tag<'a>, range: Range<usize>)

作用:处理 Markdown 开始标签,比如段落开始、标题开始、列表开始、链接开始、表格开始。它把“进入某种结构”转换成状态变化。

数据流:进去一个 Tag 和原文范围 → 按标签类型调用对应 start_* 函数或压入样式、链接状态 → Writer 进入相应上下文。

调用关系:handle_event 遇到 Event::Start 时调用它;它会分派到 start_paragraph、start_heading、start_table_row 等具体函数。

调用图:调用 12 个内部函数(push_inline_style, push_link, start_blockquote, start_codeblock, start_heading, start_item, start_list, start_paragraph, start_table, start_table_cell (+2 more));被 1 处调用(handle_event);外部调用 1 个(from)。

Writer::end_tag516–545 ↗
fn end_tag(&mut self, tag: TagEnd)

作用:处理 Markdown 结束标签,比如段落结束、列表项结束、链接结束、表格结束。它把当前结构收尾并恢复状态。

数据流:进去一个 TagEnd → 按类型关闭段落、标题、引用、代码块、列表、链接、表格等 → 可能输出行、弹出缩进或样式、保存表格内容。

调用关系:handle_event 遇到 Event::End 时调用它;结束链接时交给 pop_link,结束表格时交给 end_table。

调用图:调用 12 个内部函数(end_blockquote, end_codeblock, end_heading, end_list, end_paragraph, end_table, end_table_cell, end_table_head, end_table_row, flush_current_line (+2 more));被 1 处调用(handle_event)。

Writer::start_paragraph547–557 ↗
fn start_paragraph(&mut self)

作用:开始一个普通段落,必要时先插入空行。表格单元格里的段落不会单独开外部行。

数据流:进去 Writer 当前状态 → 如果在表格单元格中就跳过;否则按 needs_newline 补空行,再开一条新行 → 标记正在段落中。

调用关系:start_tag 遇到 Paragraph 时调用它,段落结束由 end_paragraph 收尾。

调用图:调用 3 个内部函数(in_table_cell, push_blank_line, push_line);被 1 处调用(start_tag);外部调用 1 个(default)。

Writer::end_paragraph559–566 ↗
fn end_paragraph(&mut self)

作用:结束一个普通段落,并告诉后面内容通常需要换行。它也会清掉列表标记行的等待状态。

数据流:进去 Writer 当前状态 → 如果在表格单元格中不处理;否则设置 needs_newline、in_paragraph 为 false → 后续块会从新行开始。

调用关系:end_tag 遇到 Paragraph 时调用它。

调用图:调用 1 个内部函数(in_table_cell);被 1 处调用(end_tag)。

Writer::start_heading568–588 ↗
fn start_heading(&mut self, level: HeadingLevel)

作用:开始标题,并按标题级别套用不同样式。它还会在显示内容前加上类似“# ”的标记。

数据流:进去标题级别 → 若不在表格里,选择对应样式,创建带 # 的新行,并把标题样式压入当前内联样式栈 → 后续标题文字会继承样式。

调用关系:start_tag 遇到 Heading 时调用它;end_heading 会弹出这里压入的样式。

调用图:调用 3 个内部函数(in_table_cell, push_inline_style, push_line);被 1 处调用(start_tag);外部调用 4 个(default, from, format!, vec!)。

Writer::end_heading590–596 ↗
fn end_heading(&mut self)

作用:结束标题,让后面内容另起一行,并取消标题文字样式。

数据流:进去 Writer 当前状态 → 如果不在表格里,设置 needs_newline 并弹出标题样式 → 后续文字恢复原样。

调用关系:end_tag 遇到 Heading 时调用它。

调用图:调用 2 个内部函数(in_table_cell, pop_inline_style);被 1 处调用(end_tag)。

Writer::start_blockquote598–611 ↗
fn start_blockquote(&mut self)

作用:开始引用块,在每行前加“> ”这样的前缀。这样读者能看出这是引用内容。

数据流:进去 Writer 当前状态 → 必要时先补空行 → 把一个带“> ”前缀的 IndentContext 压入缩进栈 → 后续行自动带引用前缀。

调用关系:start_tag 遇到 BlockQuote 时调用它;end_blockquote 会弹出这层缩进。

调用图:调用 3 个内部函数(new, in_table_cell, push_blank_line);被 1 处调用(start_tag);外部调用 1 个(vec!)。

Writer::end_blockquote613–619 ↗
fn end_blockquote(&mut self)

作用:结束引用块,移除“> ”前缀,并让后面内容换行。

数据流:进去 Writer 当前状态 → 如果不在表格里,弹出引用缩进 → 设置 needs_newline。

调用关系:end_tag 遇到 BlockQuote 时调用它。

调用图:调用 1 个内部函数(in_table_cell);被 1 处调用(end_tag)。

Writer::text621–673 ↗
fn text(&mut self, text: CowStr<'a>)

作用:处理普通文本内容,是最常见的写字入口。它会根据当前位置决定写到正文、代码缓冲区还是表格单元格。

数据流:进去一段文本 → 如果本地链接标签被隐藏就丢弃;如果在表格里写入单元格;如果在带语言代码块里先缓存;否则按行拆开并应用当前样式 → 输出行或缓冲区被更新。

调用关系:handle_event 遇到 Event::Text 时调用它;它会用 push_text_spans 或 push_text_to_table_cell 真正写入。

调用图:调用 5 个内部函数(in_table_cell, push_line, push_text_spans, push_text_to_table_cell, suppressing_local_link_label);被 1 处调用(handle_event);外部调用 2 个(lines, default)。

Writer::code675–691 ↗
fn code(&mut self, code: CowStr<'a>)

作用:处理行内代码,也就是 Markdown 反引号里的短代码。它会用代码样式显示。

数据流:进去代码文本 → 如果本地链接标签被隐藏就不写;在表格里写到单元格,否则写到当前正文行 → 内容带上 code 样式。

调用关系:handle_event 遇到 Event::Code 时调用它;底层通过 push_span 或 push_span_to_table_cell 添加文字。

调用图:调用 5 个内部函数(in_table_cell, push_line, push_span, push_span_to_table_cell, suppressing_local_link_label);被 1 处调用(handle_event);外部调用 3 个(into_string, default, from)。

Writer::html693–724 ↗
fn html(&mut self, html: CowStr<'a>, inline: bool)

作用:处理 Markdown 中的 HTML 文本。这里不会真正解析 HTML,而是按文本显示,并处理行内或块级换行差异。

数据流:进去 HTML 文本和是否行内 → 表格里按行写入单元格,块级 HTML 后加硬换行;正文里按行写入当前输出 → 更新 needs_newline。

调用关系:handle_event 遇到 Html 或 InlineHtml 时调用它。

调用图:调用 6 个内部函数(in_table_cell, push_line, push_span, push_span_to_table_cell, push_table_cell_hard_break, suppressing_local_link_label);被 1 处调用(handle_event);外部调用 3 个(lines, default, styled)。

Writer::hard_break726–736 ↗
fn hard_break(&mut self)

作用:处理强制换行,相当于用户明确要求这里断行。表格内外有不同的换行方式。

数据流:进去 Writer 当前状态 → 如果在表格单元格里让单元格换行;否则开一条新输出行 → 当前链接结尾状态被清掉。

调用关系:handle_event 遇到 Event::HardBreak 时调用它。

调用图:调用 4 个内部函数(in_table_cell, push_line, push_table_cell_hard_break, suppressing_local_link_label);被 1 处调用(handle_event);外部调用 1 个(default)。

Writer::soft_break738–754 ↗
fn soft_break(&mut self)

作用:处理普通软换行。它通常变成新行,但在表格里变空格;本地文件链接后还有特殊延迟判断。

数据流:进去 Writer 当前状态 → 表格里追加空格;如果刚输出本地链接目标,则先挂起决定;否则直接开新行 → 状态按情况更新。

调用关系:handle_event 遇到 Event::SoftBreak 时调用它;挂起的情况由 prepare_for_event 在下个事件前处理。

调用图:调用 4 个内部函数(in_table_cell, push_line, push_span_to_table_cell, suppressing_local_link_label);被 1 处调用(handle_event);外部调用 2 个(default, styled)。

Writer::start_list756–762 ↗
fn start_list(&mut self, index: Option<u64>)

作用:开始一个列表,并记录这是有序列表还是无序列表。有序列表会记住当前编号。

数据流:进去可选起始编号 → 顶层列表前可能补空行 → 把编号状态和“下一项前是否要空行”的状态压栈 → 后续列表项可生成正确标记。

调用关系:start_tag 遇到 List 时调用它;每个 Item 由 start_item 处理,列表结束由 end_list 处理。

调用图:调用 1 个内部函数(push_line);被 1 处调用(start_tag);外部调用 1 个(default)。

Writer::end_list764–768 ↗
fn end_list(&mut self)

作用:结束当前列表,弹出列表编号和空行状态。它还让后续块通常另起一行。

数据流:进去 Writer 当前状态 → 弹出 list_indices 和 list_needs_blank_before_next_item → 设置 needs_newline。

调用关系:end_tag 遇到 List 时调用它。

调用图:被 1 处调用(end_tag)。

Writer::start_item770–818 ↗
fn start_item(&mut self)

作用:开始一个列表项,生成“- ”或“1. ”这样的列表标记,并准备缩进。嵌套列表也靠它排齐。

数据流:进去 Writer 当前列表状态 → 必要时插空行并刷出上一行 → 计算层级、编号、标记文字和后续缩进 → 把列表缩进上下文压栈。

调用关系:start_tag 遇到 Item 时调用它;Item 结束时 end_tag 会刷行、判断是否需要在下一项前加空行并弹出缩进。

调用图:调用 3 个内部函数(new, flush_current_line, push_blank_line);被 1 处调用(start_tag);外部调用 2 个(new, vec!)。

Writer::start_codeblock820–845 ↗
fn start_codeblock(&mut self, lang: Option<String>, indent: Option<Span<'static>>)

作用:开始代码块,准备按代码原样显示。带语言名的围栏代码块会先缓存,结束时做语法高亮。

数据流:进去可选语言名和可选缩进 → 刷掉当前行、必要时加空行 → 解析语言名第一段、清空代码缓冲区、压入代码块缩进 → 标记进入代码块。

调用关系:start_tag 遇到 CodeBlock 时调用它;end_codeblock 负责输出或高亮缓存内容。

调用图:调用 3 个内部函数(new, flush_current_line, push_blank_line);被 1 处调用(start_tag);外部调用 1 个(vec!)。

Writer::end_codeblock847–865 ↗
fn end_codeblock(&mut self)

作用:结束代码块。若之前知道代码语言,就把缓存内容做语法高亮后输出。

数据流:进去 Writer 当前代码块状态 → 如果有语言和缓存代码,调用 highlight_code_to_lines 生成彩色行并输出 → 清除代码块状态、弹出缩进、设置需要换行。

调用关系:end_tag 遇到 CodeBlock 时调用它;它会把最终代码行交给 push_line 和 push_span。

调用图:调用 3 个内部函数(push_line, push_span, highlight_code_to_lines);被 1 处调用(end_tag);外部调用 2 个(default, take)。

Writer::start_table867–874 ↗
fn start_table(&mut self, alignments: Vec<Alignment>)

作用:开始收集一个 Markdown 表格。表格不会马上输出,而是先攒完整再统一排版。

数据流:进去列对齐设置 → 刷当前行,必要时补空行 → 创建 TableState 保存表格临时数据 → Writer 进入表格模式。

调用关系:start_tag 遇到 Table 时调用它;end_table 会把收集好的表格真正渲染出来。

调用图:调用 3 个内部函数(new, flush_current_line, push_blank_line);被 1 处调用(start_tag)。

Writer::end_table876–902 ↗
fn end_table(&mut self)

作用:结束表格并把它排成最终显示行。它会选择普通表格、预换行表格或溢出文本等输出方式。

数据流:进去 Writer 当前表格状态 → 取出 TableState → 调用 render_table_lines 生成表格行和溢出行 → 按是否已预换行分别输出 → 设置后续需要换行。

调用关系:end_tag 遇到 Table 时调用它;核心排版交给 render_table_lines。

调用图:调用 4 个内部函数(flush_current_line, push_hyperlink_line, push_prewrapped_line, render_table_lines);被 1 处调用(end_tag)。

Writer::start_table_head904–909 ↗
fn start_table_head(&mut self)

作用:开始表头行的收集。表头通常会用更醒目的样式显示。

数据流:进去 Writer 当前表格状态 → 标记 in_header 为 true,并创建新的当前行 → 后续单元格会进入表头。

调用关系:start_tag 遇到 TableHead 时调用它;end_table_head 会把当前行保存成 header。

调用图:被 1 处调用(start_tag);外部调用 1 个(new)。

Writer::end_table_head911–925 ↗
fn end_table_head(&mut self)

作用:结束表头收集,把最后一个单元格和当前行保存为表头。

数据流:进去表格状态 → 如果还有当前单元格就放入当前行 → 把当前行取出保存为 header → 关闭 in_header 标记。

调用关系:end_tag 遇到 TableHead 时调用它。

调用图:被 1 处调用(end_tag)。

Writer::start_table_row927–933 ↗
fn start_table_row(&mut self, source_range: Range<usize>)

作用:开始收集表格的一行,并记住原文这一行有没有管道符边界。这个信息用来判断某些“伪表格行”。

数据流:进去原文范围 → 检查这一段源码是否以 | 开头或结尾 → 创建空当前行并记录管道符语法标记。

调用关系:start_tag 遇到 TableRow 时调用它;检查工作交给 has_table_row_boundary_pipe。

调用图:调用 1 个内部函数(has_table_row_boundary_pipe);被 1 处调用(start_tag);外部调用 1 个(new)。

Writer::has_table_row_boundary_pipe935–941 ↗
fn has_table_row_boundary_pipe(&self, source_range: Range<usize>) -> bool

作用:判断表格行原文是不是明显使用了“|”管道表格写法。它帮助区分真正表格和被解析器误吞进去的普通段落。

数据流:进去源码范围 → 从原始输入中取出对应文本并去掉两端空白 → 看是否以 | 开头或结尾 → 返回布尔值。

调用关系:start_table_row 调用它,把结果存进 TableState。

调用图:被 1 处调用(start_table_row)。

Writer::end_table_row943–968 ↗
fn end_table_row(&mut self)

作用:结束当前表格行,把它放到表头或表体里。

数据流:进去表格状态 → 若当前单元格还没入行就先放入 → 取出当前行 → 根据是否在表头保存为 header 或追加到 rows → 清掉本行管道标记。

调用关系:end_tag 遇到 TableRow 时调用它。

调用图:被 1 处调用(end_tag)。

Writer::start_table_cell970–974 ↗
fn start_table_cell(&mut self)

作用:开始一个新的表格单元格,准备接收里面的文本、链接或换行。

数据流:进去表格状态 → 创建默认 TableCell 作为 current_cell → 后续内容写进这个单元格。

调用关系:start_tag 遇到 TableCell 时调用它;end_table_cell 会把它放入当前行。

调用图:被 1 处调用(start_tag);外部调用 1 个(default)。

Writer::end_table_cell976–987 ↗
fn end_table_cell(&mut self)

作用:结束当前表格单元格,并把它加入当前表格行。

数据流:进去表格状态 → 取出 current_cell → 确保有 current_row 并把单元格推进去 → 当前单元格清空。

调用关系:end_tag 遇到 TableCell 时调用它。

调用图:被 1 处调用(end_tag)。

Writer::in_table_cell989–994 ↗
fn in_table_cell(&self) -> bool

作用:判断当前是不是正在写表格单元格。很多处理逻辑要根据这个结果走不同路线。

数据流:进去 Writer 状态 → 查看 table_state 里是否有 current_cell → 返回 true 或 false。

调用关系:text、code、html、换行、链接收尾等函数都会用它决定写到表格里还是正文里。

调用图:被 12 处调用(code, end_blockquote, end_heading, end_paragraph, hard_break, html, pop_link, soft_break, start_blockquote, start_heading (+2 more))。

Writer::push_span_to_table_cell996–1002 ↗
fn push_span_to_table_cell(&mut self, span: Span<'static>)

作用:把一小段带样式文字写进当前表格单元格。没有当前单元格时它什么也不做。

数据流:进去 Span → 找到 table_state.current_cell → 调用单元格的 push_span 追加内容 → 单元格文字增加。

调用关系:code、html、soft_break、pop_link 等在表格内部写内容时调用它。

调用图:被 4 处调用(code, html, pop_link, soft_break)。

Writer::push_table_cell_hard_break1004–1010 ↗
fn push_table_cell_hard_break(&mut self)

作用:在当前表格单元格里插入硬换行。它只影响单元格内部,不会直接输出整张表。

数据流:进去 Writer 状态 → 找到当前单元格 → 调用 TableCell::hard_break → 单元格新增一行。

调用关系:hard_break、html、push_text_to_table_cell 会调用它。

调用图:被 3 处调用(hard_break, html, push_text_to_table_cell)。

Writer::push_text_to_table_cell1012–1020 ↗
fn push_text_to_table_cell(&mut self, text: &str)

作用:把普通文本写入表格单元格,并保留文本里的换行。

数据流:进去字符串 → 取当前内联样式 → 按行拆分,行与行之间插入单元格硬换行 → 每行交给 push_text_spans_to_table_cell 处理链接和样式。

调用关系:Writer::text 在发现自己位于表格单元格中时调用它。

调用图:调用 2 个内部函数(push_table_cell_hard_break, push_text_spans_to_table_cell);被 1 处调用(text)。

Writer::push_text_spans_to_table_cell1022–1042 ↗
fn push_text_spans_to_table_cell(&mut self, text: &str, style: Style)

作用:把一行表格单元格文本变成带样式、可能带链接标注的内容。它也会自动识别裸露的网页地址。

数据流:进去文本和样式 → 如果当前 Markdown 链接是网页,整段标注为该链接;如果在链接或代码块里就不自动识别;否则自动识别网页 URL → 把结果追加到当前单元格。

调用关系:push_text_to_table_cell 调用它;它使用 annotate_web_urls_in_line 给普通文本里的网址加注解。

调用图:调用 2 个内部函数(new, annotate_web_urls_in_line);被 1 处调用(push_text_to_table_cell);外部调用 4 个(default, from, styled, take)。

Writer::render_table_lines1055–1177 ↗
fn render_table_lines(&self, mut table_state: TableState) -> RenderedTableLines

作用:把已经收集好的表格数据排成最终要显示的多行文字。这是表格渲染的核心决策点。

数据流:进去 TableState → 过滤被解析器误当表格的溢出行,补齐列数,统计每列宽度和内容类型 → 根据可用宽度选择列宽、键值记录形式或管道表格回退 → 出来 RenderedTableLines。

调用关系:end_table 调用它;它会进一步调用 compute_column_widths、render_table_row、render_table_pipe_fallback 以及 table_key_value 的记录渲染逻辑。

调用图:调用 9 个内部函数(available_record_width, available_table_width, compute_column_widths, render_table_pipe_fallback, render_table_row, render_records, should_render_records, foreground_style_for_scopes, table_separator_style);被 1 处调用(end_table);外部调用 7 个(collect_table_column_metrics, is_spillover_row, normalize_row, render_table_separator, default, new, with_capacity)。

Writer::normalize_row1179–1182 ↗
fn normalize_row(row: &mut Vec<TableCell>, column_count: usize)

作用:把一行表格修正成固定列数。多余单元格丢掉,缺少的补空白。

数据流:进去一行单元格和目标列数 → truncate 截断多余列,resize 补默认空单元格 → 这行变得和表头列数一致。

调用关系:render_table_lines 在真正计算宽度前使用它,避免后面按列访问时错位。

调用图:外部调用 1 个(default)。

Writer::available_table_width1185–1194 ↗
fn available_table_width(&self, column_count: usize) -> Option<usize>

作用:计算表格内容区最多能占多少宽度。它会扣掉列表、引用前缀、列间距和单元格内边距。

数据流:进去列数 → 如果有 wrap_width,就算出当前前缀宽度和表格固定开销 → 返回剩余宽度;没有 wrap_width 就返回 None。

调用关系:render_table_lines 调用它,为 compute_column_widths 提供宽度预算。

调用图:被 1 处调用(render_table_lines)。

Writer::available_record_width1197–1203 ↗
fn available_record_width(&self) -> Option<usize>

作用:计算键值记录式表格可用的整行宽度。它只扣掉当前缩进前缀。

数据流:进去 Writer 状态 → 如果有 wrap_width,减去 prefix_spans 的显示宽度 → 返回可用宽度;否则返回 None。

调用关系:render_table_lines 在决定使用 table_key_value::render_records 时调用它。

调用图:被 1 处调用(render_table_lines)。

Writer::compute_column_widths1212–1276 ↗
fn compute_column_widths(
        &self,
        header: &[TableCell],
        rows: &[Vec<TableCell>],
        alignments: &[Alignment],
        available_width: Option<usize>,
    ) -> Option<Vec<us

作用:根据内容和屏幕宽度给每个表格列分配宽度。它会尽量保留重要长词,又在空间不够时逐步压缩列。

数据流:进去表头、表体、对齐方式、可用宽度 → 收集列指标,先用最大内容宽度,再按最小宽度和偏好下限收缩 → 成功返回每列宽度,太窄则返回 None。

调用关系:render_table_lines 调用它;它依赖 collect_table_column_metrics、preferred_column_floor、next_column_to_shrink。

调用图:被 1 处调用(render_table_lines);外部调用 3 个(len, collect_table_column_metrics, next_column_to_shrink)。

Writer::collect_table_column_metrics1278–1343 ↗
fn collect_table_column_metrics(
        header: &[TableCell],
        rows: &[Vec<TableCell>],
        column_count: usize,
    ) -> Vec<TableColumnMetrics>

作用:统计每一列表格内容的特征,比如最宽内容、最长单词、是不是长篇描述列。后面分配列宽要靠这些信息。

数据流:进去表头、表体和列数 → 逐列扫描纯文字和显示宽度,统计词数、长词数量、平均长度 → 出来一组 TableColumnMetrics。

调用关系:render_table_lines 和 compute_column_widths 都会用它,为宽度和布局选择提供依据。

调用图:外部调用 3 个(cell_display_width, longest_token_width, with_capacity)。

Writer::preferred_column_floor1351–1359 ↗
fn preferred_column_floor(metrics: &TableColumnMetrics, min_column_width: usize) -> usize

作用:给某一列算一个“最好别再窄”的宽度下限。这样压缩表格时不会轻易把长词或描述压得太碎。

数据流:进去列指标和硬性最小宽度 → 根据列类型选择目标宽度,再限制在最小值和最大内容宽度之间 → 返回偏好的下限。

调用关系:compute_column_widths 在压缩列宽前调用它生成 floors。

Writer::next_column_to_shrink1366–1383 ↗
fn next_column_to_shrink(
        widths: &[usize],
        floors: &[usize],
        metrics: &[TableColumnMetrics],
    ) -> Option<usize>

作用:在需要缩窄表格时,挑下一列来减少宽度。它会优先缩那些更适合换行的列。

数据流:进去当前宽度、每列下限和列指标 → 过滤还能缩的列 → 按列类型优先级和剩余空间选择一列 → 返回列索引或 None。

调用关系:compute_column_widths 的收缩循环反复调用它。

Writer::column_shrink_priority1385–1391 ↗
fn column_shrink_priority(kind: TableColumnKind) -> usize

作用:给不同类型的列排压缩优先级。长令牌列、叙述列、紧凑列会按不同顺序被压缩。

数据流:进去 TableColumnKind → 返回数字优先级,数字越小越先被考虑 → 供列宽收缩算法排序。

调用关系:next_column_to_shrink 和 compute_column_widths 内部选择收缩对象时使用它。

Writer::render_table_separator1393–1406 ↗
fn render_table_separator(
        column_widths: &[usize],
        separator_char: char,
        style: Style,
    ) -> HyperlinkLine

作用:生成表格中的分隔线,比如表头下面的一排横线。它会按每列宽度拼出合适长度。

数据流:进去列宽、分隔字符和样式 → 为每列重复分隔字符并加上单元格 padding 和列间距 → 出来一条 HyperlinkLine。

调用关系:render_table_lines 在表头后和表体行之间调用它。

调用图:调用 1 个内部函数(new);外部调用 2 个(from, styled)。

Writer::render_table_row1408–1493 ↗
fn render_table_row(
        &self,
        row: &[TableCell],
        column_widths: &[usize],
        alignments: &[Alignment],
        row_style: Style,
    ) -> Vec<HyperlinkLine>

作用:把一行表格单元格渲染成一行或多行显示文字,并按左中右对齐补空格。

数据流:进去一行单元格、列宽、对齐方式、整行样式 → 先把每个单元格按列宽换行 → 逐显示行拼接各列、补 padding、调整链接列位置 → 出来多条 HyperlinkLine。

调用关系:render_table_lines 用它渲染表头和每一行表体;单元格换行交给 wrap_cell。

调用图:调用 1 个内部函数(new);被 1 处调用(render_table_lines);外部调用 7 个(default, from, line_display_width, raw, new, with_capacity, iter)。

Writer::render_table_pipe_fallback1500–1513 ↗
fn render_table_pipe_fallback(
        &self,
        header: &[TableCell],
        rows: &[Vec<TableCell>],
        alignments: &[Alignment],
    ) -> Vec<HyperlinkLine>

作用:当宽度太窄或没有表体时,把表格退回成原始 Markdown 管道表格样式。这样至少还能完整表达表格结构。

数据流:进去表头、表体和对齐方式 → 生成表头管道行、对齐分隔行、每个表体管道行 → 出来多条 HyperlinkLine。

调用关系:render_table_lines 在无法正常分配列宽的某些情况下调用它。

调用图:调用 1 个内部函数(new);被 1 处调用(render_table_lines);外部调用 4 个(from, alignments_to_pipe_delimiter, row_to_pipe_line, new)。

Writer::row_to_pipe_line1515–1562 ↗
fn row_to_pipe_line(row: &[TableCell]) -> HyperlinkLine

作用:把一行表格单元格变成“| a | b |”这种管道文本,同时保留网页链接标注。单元格里的竖线会被转义,避免看起来像分列。

数据流:进去一行 TableCell → 逐单元格拼文字,多行用空格合并,遇到 | 写成 \|,并按原链接范围给输出片段加链接 → 出来一条 HyperlinkLine。

调用关系:render_table_pipe_fallback 调用它生成表头和表体的管道行。

调用图:调用 1 个内部函数(new);外部调用 3 个(default, new, width)。

Writer::alignments_to_pipe_delimiter1564–1578 ↗
fn alignments_to_pipe_delimiter(alignments: &[Alignment]) -> String

作用:把列对齐方式变成 Markdown 管道表格里的分隔行,比如左对齐是“:---”。

数据流:进去对齐方式数组 → 逐列追加对应分隔片段并用 | 包起来 → 出来一个分隔行字符串。

调用关系:render_table_pipe_fallback 调用它生成表头下面的对齐说明行。

调用图:外部调用 1 个(new)。

Writer::wrap_cell1586–1607 ↗
fn wrap_cell(&self, cell: &TableCell, width: usize) -> Vec<HyperlinkLine>

作用:把一个表格单元格按指定列宽折行,同时保持链接仍指向正确文字。它让窄屏表格能读。

数据流:进去单元格和宽度 → 对单元格每一行调用 word_wrap_line 换行 → 用 remap_wrapped_line 重映射链接位置 → 出来换行后的 HyperlinkLine 列表。

调用关系:render_table_row 在渲染每个单元格前调用它。

调用图:调用 4 个内部函数(new, remap_wrapped_line, new, word_wrap_line);外部调用 3 个(default, new, vec!)。

Writer::is_spillover_row1620–1652 ↗
fn is_spillover_row(row: &TableBodyRow, next_row: Option<&TableBodyRow>) -> bool

作用:判断某一行是不是被 Markdown 解析器误当成表格行的普通内容。常见情况是表格后面的段落或 HTML 被吞进表格。

数据流:进去当前表体行和下一行 → 找第一列非空且其他列为空的文字,再结合是否有管道符、是否像 HTML、是否像“HTML:”标签判断 → 返回是否应作为表格外溢出文本。

调用关系:render_table_lines 在整理表体行时调用它,把误入表格的内容移到 spillover_lines。

调用图:外部调用 3 个(first_non_empty_only_text, looks_like_html_content, looks_like_html_label_line)。

Writer::first_non_empty_only_text1654–1663 ↗
fn first_non_empty_only_text(row: &[TableCell]) -> Option<String>

作用:检查一行表格是否只有第一格有文字,其他格都空。这个形态常用于识别表格外溢内容。

数据流:进去一行单元格 → 取第一格纯文字,若为空返回 None;再确认后续单元格都为空 → 成功返回第一格文字。

调用关系:is_spillover_row 调用它做第一步筛选。

调用图:外部调用 1 个(first)。

Writer::looks_like_html_content1665–1686 ↗
fn looks_like_html_content(text: &str) -> bool

作用:粗略判断一段文字里是否像 HTML 标签,比如“<div>”。它不是完整 HTML 解析,只是识别明显特征。

数据流:进去字符串 → 扫描字符里的 <,允许后面有 / 或 !,再看是否跟字母且后面有 > → 返回是否像 HTML。

调用关系:is_spillover_row 用它判断表格后面的 HTML 内容是否被误收进表格。

Writer::looks_like_html_label_line1688–1697 ↗
fn looks_like_html_label_line(text: &str) -> bool

作用:判断一行是否像“HTML block:”这样的说明标签。它用于把表格末尾的 HTML 引导文字从表格里挪出来。

数据流:进去字符串 → 去空白,检查是否以冒号结尾,再看冒号前是否有单词 html(忽略大小写)→ 返回布尔值。

调用关系:is_spillover_row 在没有下一行可参考时调用它。

Writer::spans_display_width1703–1705 ↗
fn spans_display_width(spans: &[Span<'_>]) -> usize

作用:计算一组 Span 在终端里实际占多少列宽。中文、emoji 等字符可能不止占一列,所以不能只数字节。

数据流:进去 Span 列表 → 对每段文字调用 width 计算显示宽度并求和 → 返回总宽度。

调用关系:available_table_width、available_record_width、line_display_width、flush_current_line 等宽度相关逻辑会用它。

调用图:外部调用 1 个(iter)。

Writer::line_display_width1708–1710 ↗
fn line_display_width(line: &Line<'_>) -> usize

作用:计算一整行在终端里显示的宽度。它把行里的多个 Span 加总。

数据流:进去 Line → 取出 spans → 调用 spans_display_width → 返回显示列数。

调用关系:render_table_row、cell_display_width 等表格布局函数用它判断内容宽度。

调用图:外部调用 1 个(spans_display_width)。

Writer::cell_display_width1713–1719 ↗
fn cell_display_width(cell: &TableCell) -> usize

作用:计算一个表格单元格最宽一行的显示宽度。列宽估算需要这个值。

数据流:进去 TableCell → 遍历单元格里的每一行,计算每行显示宽度 → 返回最大值,没有内容则返回 0。

调用关系:collect_table_column_metrics 用它统计每列最大宽度。

Writer::longest_token_width1722–1724 ↗
fn longest_token_width(text: &str) -> usize

作用:找出一段文本里最长单词或连续片段的显示宽度。长 URL、路径、标识符会影响表格换行策略。

数据流:进去字符串 → 按空白拆成 token → 计算每个 token 的显示宽度并取最大 → 返回最大值或 0。

调用关系:collect_table_column_metrics 用它识别长词较多的列。

Writer::push_inline_style1726–1730 ↗
fn push_inline_style(&mut self, style: Style)

作用:进入加粗、斜体、标题等内联样式时,把新样式叠加到当前样式栈。这样嵌套样式能同时生效。

数据流:进去一个 Style → 取当前栈顶样式,与新样式 patch 合并 → 把合并结果压入 inline_styles。

调用关系:start_heading 和 start_tag 处理 Emphasis、Strong、Strikethrough 时调用它;对应结束时 pop_inline_style 弹出。

调用图:被 2 处调用(start_heading, start_tag)。

Writer::pop_inline_style1732–1734 ↗
fn pop_inline_style(&mut self)

作用:离开当前内联样式范围时,恢复上一层样式。

数据流:进去 Writer 样式栈 → 弹出栈顶 → 后续文字使用新的栈顶样式或默认样式。

调用关系:end_heading 和 end_tag 处理强调、加粗、删除线结束时调用它。

调用图:被 2 处调用(end_heading, end_tag)。

Writer::flush_current_line1808–1841 ↗
fn flush_current_line(&mut self)

作用:把正在拼的一行真正送进输出列表。它还会按屏幕宽度自动换行,并修正缩进和链接位置。

数据流:进去 current_line_content → 如果不是代码块且设置了宽度,就按缩进自适应换行并重映射链接;否则把初始缩进直接拼到行首并右移链接列号 → 输出到 text,并清空当前行状态。

调用关系:run 结束、push_line 开新行、end_table、start_codeblock 等很多地方都会调用它,确保上一行不会丢。

调用图:调用 4 个内部函数(push_output_line, remap_wrapped_line, new, adaptive_wrap_line);被 10 处调用(end_table, end_tag, handle_event, push_blank_line, push_line, push_prewrapped_line, run, start_codeblock, start_item, start_table);外部调用 1 个(from_iter)。

Writer::is_blockquote_active1850–1854 ↗
fn is_blockquote_active(&self) -> bool

作用:判断当前是否处在引用块里。引用块里的行需要套用引用样式。

数据流:进去缩进栈 → 查找是否有前缀包含“>” → 返回 true 或 false。

调用关系:push_line 和 push_prewrapped_line 用它决定是否给行加 blockquote 样式。

调用图:被 2 处调用(push_line, push_prewrapped_line)。

Writer::push_prewrapped_line1856–1873 ↗
fn push_prewrapped_line(&mut self, mut line: HyperlinkLine, pending_marker_line: bool)

作用:输出一条已经提前排好换行的行,比如表格行。它只负责加当前缩进和修正链接位置,不再重新换行。

数据流:进去 HyperlinkLine 和是否还在等待列表标记 → 先刷当前行,计算引用样式和前缀 → 把前缀拼到行首并整体右移链接列号 → 直接推入输出。

调用关系:end_table 在表格已经预换行时调用它。

调用图:调用 5 个内部函数(flush_current_line, is_blockquote_active, prefix_spans, push_output_line, style);被 1 处调用(end_table);外部调用 1 个(from)。

Writer::push_line1875–1893 ↗
fn push_line(&mut self, line: Line<'static>)

作用:开始拼一条新的当前行。它会先把旧行刷出去,再准备本行的缩进、样式和换行信息。

数据流:进去 Line → flush_current_line 输出旧行 → 根据引用状态和 pending_marker_line 生成初始/后续缩进 → 创建 current_line_content → 清掉列表标记等待状态。

调用关系:大量内容写入函数都会用它开新行,比如 text、code、hard_break、pop_link、prepare_for_event。

调用图:调用 4 个内部函数(flush_current_line, is_blockquote_active, prefix_spans, new);被 16 处调用(code, end_codeblock, handle_event, hard_break, html, pop_link, prepare_for_event, push_annotated, push_blank_line, push_hyperlink_line (+6 more))。

Writer::push_span1903–1909 ↗
fn push_span(&mut self, span: Span<'static>)

作用:往当前行追加一小段文字;如果还没有当前行,就先创建一行。

数据流:进去 Span → 如果 current_line_content 存在就追加到它的 Line;否则创建只含这段文字的新行 → 当前行内容增加。

调用关系:code、html、end_codeblock、pop_link 等需要直接写一段文字时调用它。

调用图:调用 1 个内部函数(push_line);被 4 处调用(code, end_codeblock, html, pop_link);外部调用 2 个(from, vec!)。

Writer::push_annotated1911–1924 ↗
fn push_annotated(&mut self, mut appended: HyperlinkLine)

作用:往当前行追加一段可能带超链接标注的内容,并把链接位置接到正确列号上。

数据流:进去 HyperlinkLine → 如果没有当前行就先开空行 → 计算当前行已有宽度作为偏移 → 追加 spans,并把新链接列范围整体右移 → 当前行保留正确链接。

调用关系:push_text_spans 和 pop_link 用它合并带链接的文字。

调用图:调用 1 个内部函数(push_line);被 2 处调用(pop_link, push_text_spans);外部调用 1 个(default)。

Writer::push_text_spans1926–1942 ↗
fn push_text_spans(&mut self, text: &str, style: Style)

作用:把普通文本变成可输出的 Span,并处理网页链接标注或自动识别裸 URL。

数据流:进去文本和样式 → 如果当前 Markdown 链接是网页,则整段绑定目标;如果在链接或代码块里则不自动识别;否则自动识别文本中的网址 → 交给 push_annotated 追加。

调用关系:Writer::text 在正文中写普通文字时调用它。

调用图:调用 3 个内部函数(push_annotated, new, annotate_web_urls_in_line);被 1 处调用(text);外部调用 3 个(default, from, styled)。

Writer::push_blank_line1944–1952 ↗
fn push_blank_line(&mut self)

作用:插入空白行,并考虑列表和引用的缩进规则。它避免空行破坏列表显示。

数据流:进去 Writer 状态 → 先刷当前行 → 如果当前缩进全是列表,就直接输出真正空行;否则开一条带上下文的空行再刷出 → 输出多一条空白行。

调用关系:handle_event 的水平线、start_paragraph、start_blockquote、start_codeblock、start_item、start_table 都可能调用它。

调用图:调用 4 个内部函数(flush_current_line, push_line, push_output_line, new);被 6 处调用(handle_event, start_blockquote, start_codeblock, start_item, start_paragraph, start_table);外部调用 1 个(default)。

Writer::push_output_line1954–1956 ↗
fn push_output_line(&mut self, line: HyperlinkLine)

作用:把最终完成的一行放进输出结果列表。它是写入 self.text 的最底层动作。

数据流:进去 HyperlinkLine → 直接 push 到 self.text → 输出结果增加一行。

调用关系:flush_current_line、push_blank_line、push_prewrapped_line 调用它。

调用图:被 3 处调用(flush_current_line, push_blank_line, push_prewrapped_line)。

Writer::prefix_spans1958–1989 ↗
fn prefix_spans(&self, pending_marker_line: bool) -> Vec<Span<'static>>

作用:根据当前缩进栈生成一行开头应该显示的前缀,比如列表标记、引用符号和缩进空格。

数据流:进去是否正在等待列表标记 → 遍历 indent_stack,按规则选择 marker 或 prefix,跳过不该重复显示的中间列表缩进 → 返回 Span 列表作为行前缀。

调用关系:push_line、push_prewrapped_line、available_table_width、available_record_width 用它计算或添加行前缀。

调用图:被 2 处调用(push_line, push_prewrapped_line);外部调用 1 个(new)。

normalize_hash_location_suffix_fragment2064–2069 ↗
fn normalize_hash_location_suffix_fragment(fragment: &str) -> Option<String>

作用:把链接里 # 后面的行号片段整理成统一格式。比如只接受像 L10 这种看起来真的是位置的片段,避免把普通章节标题误当成行号。

数据流:进去的是井号后面的片段文本。它先用规则检查这个片段是不是位置标记,是的话补回 #,再交给更通用的位置后缀整理函数处理。出来的是整理好的位置字符串;如果不像行号位置,就返回空。

调用关系:它只被 parse_local_link_target 使用,专门服务 #L.. 这种链接形式。它在冒号行号解析之前运行,避免 path#L10 被误看成普通路径的一部分。

调用图:被 1 处调用(parse_local_link_target)。

extract_colon_location_suffix2075–2080 ↗
fn extract_colon_location_suffix(path_text: &str) -> Option<String>

作用:从路径末尾抠出 :10、:10:5 这类冒号形式的位置后缀。这样 “文件名:行号” 能被识别成跳转位置,而不是完整文件名。

数据流:进去的是路径文本。它用匹配规则查找冒号位置标记,并且要求这个标记必须刚好在字符串末尾。出来的是这个后缀字符串;如果末尾没有合法后缀,就返回空。

调用关系:它由 parse_local_link_target 调用,只在没有找到 #L.. 形式位置时使用。它帮解析流程兼容常见的编辑器/终端写法。

调用图:被 1 处调用(parse_local_link_target)。

file_url_to_local_path_text2103–2124 ↗
fn file_url_to_local_path_text(url: &Url) -> Option<String>

作用:把 file:// 形式的网址变成本地文件路径。没有它,Markdown 里的 file:// 链接在终端里就很难显示成用户熟悉的路径。

数据流:进去的是已经解析好的 Url 对象。它先尝试用标准方式转成本地文件路径;如果失败,就手动拼回路径,兼容网络共享路径和 Windows 盘符路径。最后统一斜杠格式。出来的是路径字符串;如果无法转换则返回空。

调用关系:它由 parse_local_link_target 在遇到 file:// 链接时调用。它负责把网址世界的话翻译成本地文件系统的话,再交回解析流程继续处理位置片段。

调用图:调用 1 个内部函数(normalize_local_link_path_text);被 1 处调用(parse_local_link_target);外部调用 5 个(host_str, path, to_file_path, format!, matches!)。

trim_trailing_local_path_separator2155–2163 ↗
fn trim_trailing_local_path_separator(path_text: &str) -> &str

作用:去掉路径末尾多余的 /,但不破坏根目录这种特殊路径。这样比较两个路径时,不会因为一个多了斜杠就认为不一样。

数据流:进去的是路径文本。它先保护 /、//、C:/ 这类根路径不被削坏;其他路径则去掉末尾的 /。出来的是借用原文本的一段切片。

调用关系:它被 strip_local_path_prefix 调用,是做路径前缀裁剪前的清理步骤。它保证 /tmp/app 和 /tmp/app/ 能被当成同一个目录来比较。

调用图:被 1 处调用(strip_local_path_prefix);外部调用 1 个(matches!)。

strip_local_path_prefix2170–2186 ↗
fn strip_local_path_prefix(path_text: &'a str, cwd_text: &str) -> Option<&'a str>

作用:如果一个绝对路径位于当前工作目录下面,就把前面的当前目录去掉,只留下相对部分。这样界面里显示的路径更短、更好读。

数据流:进去的是目标路径和当前工作目录路径。它先去掉两边末尾多余斜杠;如果两者完全相同,就返回空,表示没剩下可显示的相对部分;如果当前目录是根目录,就特殊处理;否则尝试裁掉当前目录加后面的 /。出来的是可显示的相对路径片段,或者空。

调用关系:它被 display_local_link_path 调用。它依赖 trim_trailing_local_path_separator 先把路径整理干净,是“把长路径变短”的核心步骤。

调用图:调用 1 个内部函数(trim_trailing_local_path_separator);被 1 处调用(display_local_link_path)。

tests::lines_to_strings2222–2232 ↗
fn lines_to_strings(text: &Text<'_>) -> Vec<String>

作用:测试用的小帮手,把渲染结果里的多段样式文字合并成普通字符串。这样测试可以只关心每一行显示了什么字。

数据流:进去的是渲染后的 Text 对象,里面每行有多个 span,也就是带样式的一小段文字。它逐行把 span 的内容拼起来。出来的是字符串列表。

调用关系:它服务很多换行和代码块测试。测试函数先调用渲染函数,再用它把结果变简单,最后和期望文本比较。

tests::wraps_plain_text_when_width_provided2235–2247 ↗
fn wraps_plain_text_when_width_provided()

作用:确认普通段落在给定宽度时会自动换行。这样终端窄的时候,文字不会横向溢出。

数据流:进去的是一段普通英文句子和宽度 16。测试渲染它,取出每行文字,再检查是否被拆成三行。结果是通过或失败的测试断言。

调用关系:它直接测试 render_markdown_text_with_width 的基础换行能力,并用 tests::lines_to_strings 辅助读取结果。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_list_items_preserving_indent2250–2258 ↗
fn wraps_list_items_preserving_indent()

作用:确认列表项换行后会保留缩进。这样第二行不会看起来像一个新的普通段落。

数据流:进去的是一个项目符号列表和宽度 14。渲染后它检查第一行带 -,续行用两个空格缩进。出来的是测试断言结果。

调用关系:它覆盖列表排版路径,调用 render_markdown_text_with_width,再借助 tests::lines_to_strings 比较最终行文本。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_nested_lists2261–2277 ↗
fn wraps_nested_lists()

作用:确认嵌套列表在窄宽度下仍然保持层级。外层和内层列表换行后都要缩进正确。

数据流:进去的是外层列表和内层列表组成的 Markdown,以及宽度 20。渲染后测试逐行检查外层续行、内层项目符号和内层续行的缩进。出来的是测试是否通过。

调用关系:它测试 render_markdown_text_with_width 对复杂列表结构的处理,确保列表嵌套不会在排版时被压扁。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_ordered_lists2280–2293 ↗
fn wraps_ordered_lists()

作用:确认有编号的列表换行后,后续行会对齐正文,而不是对齐到最左边。这样编号列表读起来仍然清楚。

数据流:进去的是一个 1. 开头的列表项和宽度 18。渲染后它检查第一行带编号,后面几行用三个空格缩进。出来的是断言结果。

调用关系:它测试 render_markdown_text_with_width 的有序列表排版规则,和无序列表测试一起守住列表显示质量。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_blockquotes2296–2308 ↗
fn wraps_blockquotes()

作用:确认引用块换行后每一行都保留 > 符号。这样用户能看出这些行都属于引用内容。

数据流:进去的是一段 > 开头的引用和宽度 22。渲染后它检查每行都以 > 加空格开头,并按宽度拆分。出来的是测试断言结果。

调用关系:它调用 render_markdown_text_with_width,专门覆盖引用块的换行前缀处理。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_blockquotes_inside_lists2311–2323 ↗
fn wraps_blockquotes_inside_lists()

作用:确认列表里面的引用块换行时,既保留列表缩进,也保留引用符号。这样混合结构不会乱套。

数据流:进去的是一个列表项和它下面的引用块,宽度 24。渲染后检查引用行前面有列表缩进和 >。出来的是测试结果。

调用关系:它测试 render_markdown_text_with_width 处理“列表套引用”的场景,是列表和引用两套排版规则的组合验证。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wraps_list_items_containing_blockquotes2326–2338 ↗
fn wraps_list_items_containing_blockquotes()

作用:确认编号列表中的引用块换行仍然对齐正确。它防止列表编号、缩进和引用符号互相干扰。

数据流:进去的是一个编号列表项,下面接引用文本,宽度 24。渲染后检查列表行和两行引用行的文本。出来的是断言通过或失败。

调用关系:它调用 render_markdown_text_with_width,覆盖“有序列表加引用”的复合排版路径。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::does_not_wrap_code_blocks2341–2349 ↗
fn does_not_wrap_code_blocks()

作用:确认代码块里的长行不会被自动换行。代码讲究原样显示,随便折行可能改变可读性甚至含义。

数据流:进去的是围栏代码块和很窄的宽度 10。渲染后测试期望仍然只有原始那一整行代码。出来的是测试断言结果。

调用关系:它测试 render_markdown_text_with_width 对代码块的特殊保护,和普通文字自动换行形成对比。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::does_not_split_long_url_like_token_without_scheme2352–2363 ↗
fn does_not_split_long_url_like_token_without_scheme()

作用:确认很长、像网址的单个词不会被硬拆开,即使它没有 https:// 这种协议开头。这样路径或接口名不会被切得难以复制。

数据流:进去的是一长串类似 URL 的文本和宽度 24。渲染后测试统计完整长串是否仍出现在同一行。出来的是断言结果。

调用关系:它测试 render_markdown_text_with_width 的分词换行策略,防止长 token 被错误切碎。

调用图:调用 1 个内部函数(render_markdown_text_with_width);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::fenced_code_info_string_with_metadata_highlights2366–2383 ↗
fn fenced_code_info_string_with_metadata_highlights()

作用:确认代码块语言后面带额外信息时,仍然能识别出语言并做语法高亮。比如 rust,no_run 也应该按 Rust 代码上色。

数据流:进去的是几种带元数据的 Rust 围栏代码块。测试渲染后检查是否有 RGB 颜色样式。出来的是高亮是否生效的断言结果。

调用关系:它调用 render_markdown_text,覆盖代码块信息字符串解析和语法高亮之间的衔接。

调用图:调用 1 个内部函数(render_markdown_text);外部调用 2 个(assert!, format!)。

tests::crlf_code_block_no_extra_blank_lines2386–2398 ↗
fn crlf_code_block_no_extra_blank_lines()

作用:确认 Windows 风格换行的代码块不会被多插入空白行。否则同一段代码在不同系统输入下显示会不一致。

数据流:进去的是带 CRLF 换行的 Rust 代码块。渲染后提取每行文字,检查只有两行代码,没有夹杂多余空行。出来的是测试断言结果。

调用关系:它测试 render_markdown_text 对代码块文本事件的拼接方式,防止底层解析器拆分文本后渲染器错误加分隔符。

调用图:调用 1 个内部函数(render_markdown_text);外部调用 2 个(assert_eq!, lines_to_strings)。

tests::wrap_cell_preserves_hard_break_lines2401–2424 ↗
fn wrap_cell_preserves_hard_break_lines()

作用:确认表格单元格里的强制换行会被保留。也就是说,用户明确分成两行的内容,不会被包装函数合成一行。

数据流:进去的是一个手工构造的表格单元格,中间有 hard_break。测试创建渲染器并调用 wrap_cell,再把结果行拼成字符串。出来的是两行文本的断言结果。

调用关系:它直接测试 W::wrap_cell,保证表格换行算法尊重单元格内部的硬换行。

调用图:外部调用 4 个(new, assert_eq!, empty, default)。

tests::make_cell2432–2436 ↗
fn make_cell(text: &str) -> TableCell

作用:测试用的小工厂,用一段普通文字快速做出一个表格单元格。这样后面的表格测试不用反复写构造代码。

数据流:进去的是文本字符串。它创建默认 TableCell,把这段文字作为普通 span 放进去。出来的是一个 TableCell。

调用关系:它被多种表格列分类和溢出行测试调用,是测试数据准备的辅助函数。

调用图:外部调用 2 个(raw, default)。

tests::make_body_row2438–2443 ↗
fn make_body_row(cells: Vec<TableCell>, has_table_pipe_syntax: bool) -> TableBodyRow

作用:测试用的小工厂,用一组单元格快速做出表格正文行。它还能标记这一行是不是来自标准的管道表格写法。

数据流:进去的是单元格列表和一个布尔值,表示是否有表格管道语法。它把两者装进 TableBodyRow。出来的是测试用表格行对象。

调用关系:它被溢出行相关测试调用,帮助构造不同形态的表格行来喂给 W::is_spillover_row。

tests::column_classification_narrative_by_word_count2448–2459 ↗
fn column_classification_narrative_by_word_count()

作用:确认表格列里如果是长句描述,会被识别成叙述型列。叙述型列通常需要更适合阅读的宽度。

数据流:进去的是两列表格:一列短 ID,一列长描述。测试收集列指标后检查第一列是 Compact,第二列是 Narrative。出来的是断言结果。

调用关系:它测试 W::collect_table_column_metrics 的列类型判断,确保表格排版能根据内容性质分配宽度。

调用图:外部调用 3 个(collect_table_column_metrics, assert_eq!, vec!)。

tests::column_classification_token_heavy_by_url_like_tokens2462–2470 ↗
fn column_classification_token_heavy_by_url_like_tokens()

作用:确认主要由长 URL 组成的列会被识别成 token-heavy,也就是长块状词很多的列。这样的列换行策略和普通句子不同。

数据流:进去的是一列表格,单元格里都是长网址。测试收集列指标,并检查列类型是 TokenHeavy。出来的是断言结果。

调用关系:它测试 W::collect_table_column_metrics 对长网址类内容的识别,帮助表格渲染避免把 URL 排得太难用。

调用图:外部调用 3 个(collect_table_column_metrics, assert_eq!, vec!)。

tests::column_classification_token_heavy_for_local_path_lists2473–2485 ↗
fn column_classification_token_heavy_for_local_path_lists()

作用:确认包含很多本地文件路径的列也会被识别成 token-heavy。文件路径和网址一样,常常是长 token,不能按普通句子处理。

数据流:进去的是一列表格,里面是多个源码文件路径和行号。测试计算列指标后检查类型是 TokenHeavy。出来的是断言结果。

调用关系:它覆盖 W::collect_table_column_metrics 对本地路径列表的判断,保证文件清单类表格排版更稳。

调用图:外部调用 3 个(collect_table_column_metrics, assert_eq!, vec!)。

tests::column_classification_compact_all_short2488–2498 ↗
fn column_classification_compact_all_short()

作用:确认全是短值的表格列会被识别成紧凑型列。这样短状态、数字这类内容不会占太多宽度。

数据流:进去的是两列短文本和数字。测试收集列指标后检查两列都是 Compact。出来的是断言结果。

调用关系:它测试 W::collect_table_column_metrics 的基础分类,和叙述型、长 token 型测试一起覆盖三种列风格。

调用图:外部调用 3 个(collect_table_column_metrics, assert_eq!, vec!)。

tests::preferred_floor_narrative_retains_readable_width2501–2517 ↗
fn preferred_floor_narrative_retains_readable_width()

作用:确认叙述型列在压缩表格宽度时,仍保留一个比较能读的最低宽度。否则长句会被切得太碎。

数据流:进去的是两组手工列指标和最小列宽 3。测试调用 preferred_column_floor,检查宽内容得到 16,较短内容保留 12。出来的是断言结果。

调用关系:它直接测试 W::preferred_column_floor,验证表格宽度分配里“最低保底宽度”的规则。

调用图:外部调用 1 个(assert_eq!)。

tests::preferred_floor_token_heavy_retains_readable_width2520–2528 ↗
fn preferred_floor_token_heavy_retains_readable_width()

作用:确认长 token 很多的列也会保留一个可用的最低宽度。这样长网址或路径不会在极窄列里变得特别难看。

数据流:进去的是一个 TokenHeavy 列指标,最大宽度很大、最长 token 很长。测试计算首选底线宽度,期望是 16。出来的是断言结果。

调用关系:它测试 W::preferred_column_floor 针对 TokenHeavy 列的规则,和叙述型列的保底测试互补。

调用图:外部调用 1 个(assert_eq!)。

tests::preferred_floor_compact_uses_body_token2531–2551 ↗
fn preferred_floor_compact_uses_body_token()

作用:确认紧凑型列的最低宽度主要看表头和正文里的短 token,并且正文 token 会有上限。这样短列不会被长偶发值撑得太夸张。

数据流:进去的是两组 Compact 列指标。第一组正文最长 token 是 12,期望底线 12;第二组正文 token 是 20,但按规则封顶到 16。出来的是两次断言结果。

调用关系:它直接测试 W::preferred_column_floor 的 Compact 分支,保证紧凑列在表格压缩时行为合理。

调用图:外部调用 1 个(assert_eq!)。

tests::next_column_to_shrink_prefers_token_heavy_then_narrative2554–2587 ↗
fn next_column_to_shrink_prefers_token_heavy_then_narrative()

作用:确认表格太宽需要缩小时,优先缩长 token 列,其次缩叙述列,最后才动紧凑列。这样最重要的短状态列更不容易被挤坏。

数据流:进去的是当前列宽、各列最低宽度和三种列指标。第一次三列都能缩,期望选 TokenHeavy;第二次 TokenHeavy 已到底线,期望选 Narrative。出来的是断言结果。

调用关系:它测试 W::next_column_to_shrink,是表格自适应宽度流程里决定“先压哪一列”的策略验证。

调用图:外部调用 2 个(next_column_to_shrink, assert_eq!)。

tests::spillover_detects_single_cell_row2592–2598 ↗
fn spillover_detects_single_cell_row()

作用:确认没有管道表格语法的单单元格行会被当成溢出行。溢出行通常不是正常表格数据,而是前面内容漏出来的一部分。

数据流:进去的是一个只有一个单元格、且没有表格管道语法的行。测试调用 is_spillover_row,期望返回真。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 的基础判断,使用 make_body_row 和 make_cell 构造输入。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_keeps_single_cell_row_with_table_pipe_syntax2601–2607 ↗
fn spillover_keeps_single_cell_row_with_table_pipe_syntax()

作用:确认如果单单元格行确实来自管道表格语法,就不要误判成溢出行。这样合法的稀疏表格不会被丢掉。

数据流:进去的是一个只有一个单元格、但标记为有管道语法的行。测试调用 is_spillover_row,期望返回假。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 对表格来源标记的尊重,避免过度清理。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_detects_html_content2610–2621 ↗
fn spillover_detects_html_content()

作用:确认只在第一格出现 HTML 内容、其他格为空的行会被识别成溢出行。这通常说明 Markdown 表格解析时把 HTML 块吞进了表格区域。

数据流:进去的是三格行,第一格是 <div>content</div>,后两格为空,且没有管道语法。测试期望 is_spillover_row 返回真。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 对 HTML 噪声行的识别,帮助表格渲染过滤异常内容。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_detects_label_followed_by_html2624–2635 ↗
fn spillover_detects_label_followed_by_html()

作用:确认像“HTML block:”这样的标签,如果下一行是 HTML,也会被当成溢出行。这样 HTML 块的说明文字不会混进表格数据。

数据流:进去的是当前行第一格为 HTML block:,下一行第一格为 <div>x</div>。测试调用 is_spillover_row 并期望为真。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 使用下一行上下文做判断,而不是只看当前行。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_detects_trailing_html_label2638–2645 ↗
fn spillover_detects_trailing_html_label()

作用:确认最后一行如果只是 HTML block: 这种尾随标签,也会被识别成溢出行。即使没有下一行,也不应把它当成正常表格数据。

数据流:进去的是第一格为 HTML block:、其他格为空的行,并且没有下一行。测试期望 is_spillover_row 返回真。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 在缺少下一行时的兜底判断。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_keeps_normal_multi_cell_row2648–2655 ↗
fn spillover_keeps_normal_multi_cell_row()

作用:确认正常多列都有内容的表格行不会被误判成溢出行。否则真正的数据会被错误过滤。

数据流:进去的是三格都非空的普通表格行。测试调用 is_spillover_row,期望返回假。出来的是断言结果。

调用关系:它给 W::is_spillover_row 提供反例测试,确保溢出检测不会太激进。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::spillover_keeps_label_when_next_is_not_html2658–2669 ↗
fn spillover_keeps_label_when_next_is_not_html()

作用:确认普通标签行后面如果不是 HTML,就不要当成溢出行。比如 Status: 后面是 ok,这是正常内容。

数据流:进去的是当前行 Status: 和下一行 ok。测试调用 is_spillover_row,期望返回假。出来的是断言结果。

调用关系:它测试 W::is_spillover_row 的上下文判断是否精确,只针对 HTML 相关异常触发。

调用图:外部调用 3 个(assert!, make_body_row, vec!)。

tests::wrapped_table_url_fragments_keep_complete_web_destination2692–2714 ↗
fn wrapped_table_url_fragments_keep_complete_web_destination()

作用:确认表格里的长网址换行后,每一段仍然保留完整点击目标。用户点任何一段,都应该打开完整网址,而不是残缺片段。

数据流:进去的是一个包含长 URL 的 Markdown 表格和很窄的宽度 32。渲染后测试找出带链接的行,确认有多行,并且每个链接目标都是完整 destination。出来的是断言结果。

调用关系:它测试 render_markdown_lines_with_width_and_cwd 在表格换行和超链接标注同时发生时的正确性。

调用图:调用 1 个内部函数(render_markdown_lines_with_width_and_cwd);外部调用 2 个(assert!, format!)。

tests::key_value_table_keeps_web_annotations2717–2734 ↗
fn key_value_table_keeps_web_annotations()

作用:确认很窄的多列表格里,网页链接标注不会丢失。即使表格排版退化或压缩,链接仍要能点。

数据流:进去的是六列表格,其中第一格是长网址,宽度只有 20。渲染后收集所有链接目标,检查非空且全部等于原网址。出来的是断言结果。

调用关系:它测试 render_markdown_lines_with_width_and_cwd 的表格链接保留能力,特别是键值型或窄表格场景。

调用图:调用 1 个内部函数(render_markdown_lines_with_width_and_cwd);外部调用 2 个(assert!, format!)。

tests::pipe_table_fallback_keeps_web_annotations2749–2770 ↗
fn pipe_table_fallback_keeps_web_annotations()

作用:确认表格在极窄宽度下走备用排版时,网页链接标注仍然正确,同时代码里的 URL 不会被标注。

数据流:进去的是一个表格:一格裸 URL,一格代码 URL,一格 Markdown 链接标签,宽度只有 5。渲染后收集链接目标,检查包含裸 URL 和 Markdown 链接目标,不包含代码 URL,也不把显示标签误当目标。出来的是断言结果。

调用关系:它测试 render_markdown_lines_with_width_and_cwd 在管道表格备用渲染路径中的链接处理,是表格、代码和链接规则的综合验证。

调用图:调用 1 个内部函数(render_markdown_lines_with_width_and_cwd);外部调用 2 个(assert!, format!)。

tui/src/markdown.rs源码 ↗
domain_logic渲染聊天消息、历史记录或测试 Markdown 显示时

终端界面不能直接显示 Markdown,它需要的是 ratatui 的 Line,也就是“终端屏幕上的一行文字”。这个文件就是入口层:普通 Markdown 直接交给渲染器;AI 回复会先过一遍“拆围栏”检查。这里的“围栏”指 Markdown 里的 `` 或 ~~~ 代码块。很多 AI 会把表格写在 ``markdown 里面,结果解析器会以为那是代码,不会画成表格。unwrap_markdown_fences 会很谨慎地看完整个代码块:只有标明 md/markdown,并且里面真的有表格的表头行和分隔行,才去掉外面的围栏;Rust、shell 等代码块不动;没写完的代码块也尽量原样显示。这样界面既能正确显示表格,又不会误伤真正的代码。

函数细节20
append_markdown34–46 ↗
fn append_markdown(
    markdown_source: &str,
    width: Option<usize>,
    cwd: Option<&Path>,
    lines: &mut Vec<Line<'static>>,
)

作用:把一段普通 Markdown 渲染成终端界面的一行行文字,并追加到已有列表里。适合已经整理好的计划块、历史内容等,不会额外拆掉代码围栏。

数据流:输入是一段 Markdown、可选宽度、可选工作目录,以及一个要追加内容的行列表。它先把 Markdown 交给 render_markdown_text_with_width_and_cwd 渲染成带样式的行,再用 push_owned_lines 把这些行复制进外部传入的列表。结果是原来的列表后面多了可直接显示的终端文字行。

调用关系:它是普通 Markdown 渲染的入口,被提交完整行、收尾并排空流式内容、以及多项测试使用。它自己不解析细节,而是把真正渲染交给 render_markdown_text_with_width_and_cwd,再把结果交给 push_owned_lines 放入输出容器。

调用图:调用 2 个内部函数(render_markdown_text_with_width_and_cwd, push_owned_lines);被 13 处调用(append_markdown_keeps_ordered_list_line_unsplit_in_context, append_markdown_matches_tui_markdown_for_ordered_item, append_markdown_preserves_full_text_line, citations_render_as_plain_text, indented_code_blocks_preserve_leading_whitespace, commit_complete_lines, finalize_and_drain, assert_streamed_equals_full, heading_not_inlined_when_split_across_chunks, loose_list_with_split_dashes_matches_full_render (+3 more))。

append_markdown_agent55–67 ↗
fn append_markdown_agent(
    markdown_source: &str,
    width: Option<usize>,
    lines: &mut Vec<Line<'static>>,
)

作用:这是测试环境下使用的 AI 回复渲染入口。它会先修正 AI 常把表格包进 markdown 代码块的问题,再渲染成终端行。

数据流:输入是 AI 回复文本、可选宽度和输出行列表。它先调用 unwrap_markdown_fences,把符合条件的 ``md 或 ``markdown 表格围栏去掉;然后调用 render_markdown_text_with_width_and_cwd 渲染;最后用 push_owned_lines 追加到输出列表。出来的是更符合用户预期的显示结果,比如表格会被画成表格而不是代码。

调用关系:它被大量围绕 AI 表格显示的测试调用,也被一些流式收集相关测试覆盖。它位于“AI 原文”和“终端渲染器”之间,先做安全的文本正规化,再把活交给通用 Markdown 渲染器。

调用图:调用 3 个内部函数(unwrap_markdown_fences, render_markdown_text_with_width_and_cwd, push_owned_lines);被 20 处调用(append_markdown_agent_keeps_markdown_fence_when_content_is_not_table, append_markdown_agent_keeps_non_markdown_fences_as_code, append_markdown_agent_unwraps_markdown_fences_for_no_outer_table_rendering, append_markdown_agent_unwraps_markdown_fences_for_single_column_table, append_markdown_agent_unwraps_markdown_fences_for_table_rendering, append_markdown_agent_unwraps_markdown_fences_for_two_column_no_outer_table, collector_source_chunks_round_trip_into_agent_fence_unwrapping, controller_handles_table_immediately_after_heading, controller_holds_blockquoted_table_tail_until_stable, controller_keeps_markdown_fenced_no_outer_tables_mutable_until_finalize (+10 more))。

unwrap_markdown_fences89–295 ↗
fn unwrap_markdown_fences(markdown_source: &'a str) -> Cow<'a, str>

作用:专门检查并去掉 AI 回复中包住表格的 ``md 或 ``markdown 围栏。这样 Markdown 表格能被当成真正表格解析,而不是被当成代码块。

数据流:输入是一整段 Markdown 字符串。它先快速检查有没有 ``` 或 ~~~,没有就直接借用原文返回,避免浪费内存;如果有,就逐行扫描,识别代码围栏的开始和结束。遇到 md/markdown 围栏时,它先暂存里面的内容,等看到结束围栏后再判断里面是否有表格结构;有表格就只输出内容、去掉围栏,没有表格就原样输出。未闭合的 markdown 围栏会把开头和内容补回去,避免半截流式文本被错误改写。输出可能是借用原文,也可能是一份新字符串。

调用关系:它被 append_markdown_agent、render_markdown_agent_with_links_and_cwd 以及多项边界测试调用。它处在渲染前的预处理位置,作用像安检员:只放行真正需要拆开的 markdown 表格围栏,其它代码块和可疑情况都尽量保持原样。

调用图:被 6 处调用(append_markdown_agent, render_markdown_agent_with_links_and_cwd, append_markdown_agent_keeps_markdown_fence_with_blank_line_between_header_and_delimiter, append_markdown_agent_keeps_non_blockquoted_markdown_fence_with_blockquote_table_example, append_markdown_agent_unwraps_blockquoted_markdown_fence_table, unwrap_markdown_fences_repro_keeps_fence_without_header_delimiter_pair);外部调用 7 个(MarkdownCandidate, Passthrough, new, Borrowed, Owned, with_capacity, new)。

tests::lines_to_strings303–313 ↗
fn lines_to_strings(lines: &[Line<'static>]) -> Vec<String>

作用:把测试里渲染出来的 Line 转成普通字符串,方便比较结果。Line 里可能有多个带样式的小片段,这个函数把它们拼回一行文字。

数据流:输入是一组 ratatui 的 Line。它遍历每一行,再遍历这一行里的所有 span,也就是一小段文字,把内容拼成 String。输出是 Vec<String>,每个字符串对应屏幕上的一行可见文字。

调用关系:它是测试辅助函数,被多个测试用来检查渲染结果。测试先调用 append_markdown 或 append_markdown_agent 得到带样式行,再交给它去掉样式外壳,只看最终文字是否正确。

调用图:外部调用 1 个(iter)。

tests::citations_render_as_plain_text316–328 ↗
fn citations_render_as_plain_text()

作用:验证类似引用标记的特殊文本不会被 Markdown 渲染器乱改。用户看到的文件引用应该保持原样。

数据流:输入是一段包含“【F:/x.rs†L1】”这类引用文字的 Markdown。测试创建空输出列表,调用 append_markdown 渲染,再用 lines_to_strings 转成普通文本,最后断言两行内容和输入期望一致。

调用关系:它直接测试 append_markdown 的普通渲染路径。它不关心围栏拆解,只确保通用渲染器不会把引用样式的文本误解析成别的东西。

调用图:调用 1 个内部函数(append_markdown);外部调用 3 个(new, assert_eq!, lines_to_strings)。

tests::indented_code_blocks_preserve_leading_whitespace331–338 ↗
fn indented_code_blocks_preserve_leading_whitespace()

作用:验证缩进代码块前面的空格不会丢。对代码来说,开头空格常常有意义,显示时不能被悄悄删掉。

数据流:输入是一段包含四个空格缩进代码行的 Markdown。测试调用 append_markdown 渲染,然后转成字符串列表,最后确认输出里仍然有“ code 1”这行,并且前后空行也保留。

调用关系:它测试 append_markdown 经过通用 Markdown 渲染器后的基础行为。这个测试保护的是“代码块显示不要破坏空白”的体验。

调用图:调用 1 个内部函数(append_markdown);外部调用 3 个(new, assert_eq!, lines_to_strings)。

tests::append_markdown_preserves_full_text_line341–360 ↗
fn append_markdown_preserves_full_text_line()

作用:验证一整行普通文本不会被无故拆开或截断。聊天界面里的普通回复应该按完整句子显示。

数据流:输入是一句较长但普通的文本。测试调用 append_markdown 渲染到输出列表,先确认只生成一行,再把这一行所有片段拼起来,确认和预期完整文本一致。

调用关系:它直接覆盖 append_markdown 的普通文本路径。它保证渲染器在没有特殊 Markdown 语法时不会制造多余换行。

调用图:调用 1 个内部函数(append_markdown);外部调用 2 个(new, assert_eq!)。

tests::append_markdown_matches_tui_markdown_for_ordered_item363–373 ↗
fn append_markdown_matches_tui_markdown_for_ordered_item()

作用:验证有序列表的一项,比如“1. Tight item”,会按一整行显示。这样列表在终端里看起来才自然。

数据流:输入是一个只有一项的有序列表 Markdown。测试调用 append_markdown,再把结果转成字符串,最后确认输出就是“1. Tight item”。

调用关系:它测试 append_markdown 对列表语法的渲染表现。这个测试防止列表编号和正文被拆成不符合预期的两行。

调用图:调用 1 个内部函数(append_markdown);外部调用 3 个(new, assert_eq!, lines_to_strings)。

tests::append_markdown_keeps_ordered_list_line_unsplit_in_context376–395 ↗
fn append_markdown_keeps_ordered_list_line_unsplit_in_context()

作用:验证有序列表放在普通文字后面时,也不会被拆成“1.”一行和内容一行。它检查的是更接近真实聊天内容的场景。

数据流:输入先有一句说明文字,下一行是“1. Tight item”。测试调用 append_markdown 渲染,转成字符串后,确认结果里能找到完整的列表行,同时确认没有出现编号和正文分离的连续两行。

调用关系:它继续保护 append_markdown 的列表显示行为。和单独列表项测试相比,它检查列表在上下文中仍然稳定。

调用图:调用 1 个内部函数(append_markdown);外部调用 3 个(new, assert!, lines_to_strings)。

tests::append_markdown_agent_unwraps_markdown_fences_for_table_rendering398–405 ↗
fn append_markdown_agent_unwraps_markdown_fences_for_table_rendering()

作用:验证 AI 把表格包在 ```markdown 里时,测试入口能拆掉围栏并把它画成表格。用户看到的应该是表格,不是一段代码。

数据流:输入是一个 markdown 代码围栏,里面有标准表格。测试调用 append_markdown_agent,转成字符串后,检查输出里有表格线条字符和表格数据位置,说明表格渲染生效。

调用关系:它测试 append_markdown_agent 的核心价值:先调用 unwrap_markdown_fences,再走 Markdown 渲染。它是表格围栏拆解功能的基本用例。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert!, lines_to_strings)。

tests::append_markdown_agent_unwraps_markdown_fences_for_no_outer_table_rendering408–424 ↗
fn append_markdown_agent_unwraps_markdown_fences_for_no_outer_table_rendering()

作用:验证没有外侧竖线的 Markdown 表格也能被识别并渲染成表格。Markdown 表格不一定都写成“| A | B |”这种完整样子。

数据流:输入是 ```md 围栏中的三列表格,表头行没有最外侧竖线。测试调用 append_markdown_agent,检查输出有表格横线和排好版的表头,同时确认原始表头文本没有以代码样子原封不动出现。

调用关系:它覆盖 append_markdown_agent 和 unwrap_markdown_fences 对宽松表格写法的支持。这个测试让围栏拆解不只适用于最标准的表格格式。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert!, lines_to_strings)。

tests::append_markdown_agent_unwraps_markdown_fences_for_two_column_no_outer_table427–435 ↗
fn append_markdown_agent_unwraps_markdown_fences_for_two_column_no_outer_table()

作用:验证两列表格即使没有外侧竖线,也会被拆围栏并正常画表。两列表格是很常见的简短说明格式。

数据流:输入是 ```md 围栏中的两列表格。测试调用 append_markdown_agent 后,把渲染结果转成字符串,确认出现表格线和排版后的“left/right”,并确认原始“A | B”没有作为普通代码行留下。

调用关系:它是对 append_markdown_agent 表格识别能力的补充测试。它确保两列这种较小表格不会被漏掉。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert!, lines_to_strings)。

tests::append_markdown_agent_unwraps_markdown_fences_for_single_column_table438–445 ↗
fn append_markdown_agent_unwraps_markdown_fences_for_single_column_table()

作用:验证单列表格也能被当作表格处理,而不是因为只有一列就被忽略。

数据流:输入是 ```md 围栏中的单列表格。测试调用 append_markdown_agent,检查输出里有表格线条,同时确认原始“| Only |”没有以代码文本形式显示。

调用关系:它测试 append_markdown_agent 通过 unwrap_markdown_fences 处理边界表格的能力。这个测试防止表格识别只照顾多列表格。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert!, lines_to_strings)。

tests::append_markdown_agent_keeps_non_markdown_fences_as_code448–461 ↗
fn append_markdown_agent_keeps_non_markdown_fences_as_code()

作用:验证 Rust 等非 Markdown 代码块不会被拆掉。真正的代码块即使看起来像表格,也应该按代码显示。

数据流:输入是 ```rust 围栏,里面故意放了像表格的文本。测试调用 append_markdown_agent 后转成字符串,确认输出就是三行原始内容,没有表格线条或表格排版。

调用关系:它保护 unwrap_markdown_fences 的保守原则:只有 md/markdown 围栏才可能被拆。append_markdown_agent 会经过这个预处理,所以必须保证代码语言不被误伤。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert_eq!, lines_to_strings)。

tests::append_markdown_agent_unwraps_blockquoted_markdown_fence_table464–471 ↗
fn append_markdown_agent_unwraps_blockquoted_markdown_fence_table()

作用:验证引用块里的 markdown 表格围栏也能被识别并拆掉。引用块就是每行前面带“>”的 Markdown 区域。

数据流:输入是每行都带“>”的 ``markdown 表格围栏。测试直接调用 unwrap_markdown_fences,然后确认结果里不再包含 ``,说明围栏被去掉。

调用关系:它直接测试 unwrap_markdown_fences,而不是完整渲染流程。它覆盖了带引用前缀时开围栏、关围栏和表格检测如何配合。

调用图:调用 1 个内部函数(unwrap_markdown_fences);外部调用 1 个(assert!)。

tests::append_markdown_agent_keeps_non_blockquoted_markdown_fence_with_blockquote_table_example474–478 ↗
fn append_markdown_agent_keeps_non_blockquoted_markdown_fence_with_blockquote_table_example()

作用:验证普通 markdown 围栏里如果只是放了一个引用形式的表格示例,不会被误拆。因为这可能是用户想展示的代码内容。

数据流:输入是普通 ```markdown 围栏,但里面的表格行都带“>”。测试调用 unwrap_markdown_fences,最后确认输出和输入完全一样。

调用关系:它测试 unwrap_markdown_fences 的谨慎边界。前一个测试是“围栏本身在引用块里”,这个测试是“围栏不在引用块里但内容像引用”,两者不能混淆。

调用图:调用 1 个内部函数(unwrap_markdown_fences);外部调用 1 个(assert_eq!)。

tests::append_markdown_agent_keeps_markdown_fence_when_content_is_not_table481–487 ↗
fn append_markdown_agent_keeps_markdown_fence_when_content_is_not_table()

作用:验证 markdown 围栏里如果不是表格,就仍然按代码块显示。比如用户可能就是想展示“bold”这段 Markdown 源码。

数据流:输入是 ```markdown 围栏,里面只有加粗语法文本。测试调用 append_markdown_agent,转成字符串后确认输出是字面量“bold”,而不是加粗后的内容。

调用关系:它覆盖 append_markdown_agent 的一个关键防误伤场景。unwrap_markdown_fences 只有看到表格结构才拆围栏,否则让后面的渲染器继续把它当代码块。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 3 个(new, assert_eq!, lines_to_strings)。

tests::unwrap_markdown_fences_repro_keeps_fence_without_header_delimiter_pair490–494 ↗
fn unwrap_markdown_fences_repro_keeps_fence_without_header_delimiter_pair()

作用:验证只有像表格的几行还不够,必须有连续的表头行和分隔行才拆围栏。这样可以避免把普通文本误判成表格。

数据流:输入是 ```markdown 围栏,里面有竖线,也有类似分隔符的行,但表头和分隔行没有形成正确相邻关系。测试调用 unwrap_markdown_fences,确认输出和输入完全相同。

调用关系:它直接保护 unwrap_markdown_fences 的表格判断规则。这个测试来自复现场景,防止以后放宽规则时重新引入误拆问题。

调用图:调用 1 个内部函数(unwrap_markdown_fences);外部调用 1 个(assert_eq!)。

tests::append_markdown_agent_keeps_markdown_fence_with_blank_line_between_header_and_delimiter497–501 ↗
fn append_markdown_agent_keeps_markdown_fence_with_blank_line_between_header_and_delimiter()

作用:验证表头和分隔行中间有空行时,不算真正的 Markdown 表格,因此围栏不能被拆。Markdown 表格要求这两行挨着。

数据流:输入是 ```markdown 围栏,表头后面空了一行才出现分隔行。测试调用 unwrap_markdown_fences,确认输出仍然等于原文。

调用关系:它直接测试 unwrap_markdown_fences 的严格性。它和其它表格拆围栏测试一起,说明这个功能不是看到竖线就拆,而是按 Markdown 表格的基本结构来判断。

调用图:调用 1 个内部函数(unwrap_markdown_fences);外部调用 1 个(assert_eq!)。

tui/src/markdown_stream.rs源码 ↗
domain_logicstreaming output / request handling

聊天或流式输出里,Markdown 内容常常不是一次性到齐,而是一小段一小段传来。这个文件里的 MarkdownStreamCollector 就像一个临时收件箱:新来的文字先放进 buffer,只有看到换行符,才认为前面的行已经完整,可以交给外面的显示逻辑重新渲染。这样做很重要,因为 Markdown 的意思经常要等一整行甚至几行才看得准,比如标题、列表、表格、代码块。如果太早显示,后面来了新内容可能会让前面的格式变样。正式运行时,它主要交出“原始 Markdown 文本片段”,不自己解析 Markdown;测试模式下,它还会直接调用渲染函数,把结果变成终端界面用的 Line,方便检查颜色、换行、列表等表现是否和一次性完整渲染一致。

函数细节35
MarkdownStreamCollector::new46–59 ↗
fn new(width: Option<usize>, cwd: &Path) -> Self

作用:创建一个新的 Markdown 流收集器。调用者用它开始接收一段新的流式 Markdown 输出。

数据流:输入显示宽度和当前目录路径 → 它准备一个空字符串作为缓存,把“已经交出去多少字节”记为 0,并在测试时保存当前目录 → 输出一个干净的新收集器。

调用关系:测试辅助函数 simulate_stream_markdown_for_tests 和多项测试都会先调用它来搭好收集器;它内部会创建空 String,并在测试环境把路径复制成 PathBuf,后续 push_delta、commit 和 finalize 都围绕这个对象工作。

调用图:被 9 处调用(simulate_stream_markdown_for_tests, collector_source_chunks_round_trip_into_agent_fence_unwrapping, finalize_commits_partial_line, heading_not_inlined_when_split_across_chunks, heading_starts_on_new_line_when_following_paragraph, no_commit_until_newline, pipe_text_without_table_prefix_is_not_delayed, table_header_commits_without_holdback, new);外部调用 2 个(to_path_buf, new)。

MarkdownStreamCollector::set_width62–64 ↗
fn set_width(&mut self, width: Option<usize>)

作用:修改测试渲染时使用的显示宽度。它主要用于测试不同终端宽度下的 Markdown 换行效果。

数据流:输入一个可选宽度 → 它把收集器里记录的 width 替换掉 → 后续测试渲染时会按这个新宽度处理。

调用关系:这是一个简单的设置入口,不把工作交给别的函数;它影响的是测试专用的 commit_complete_lines 和 finalize_and_drain 的渲染结果。

MarkdownStreamCollector::clear67–74 ↗
fn clear(&mut self)

作用:把收集器清空,准备接下一段新内容。它用于流结束后或需要丢弃当前缓存时恢复初始状态。

数据流:读取当前缓存、已提交位置和测试里的已提交行数 → 清空文字缓存,把计数归零 → 收集器回到刚创建时的状态。

调用关系:finalize_and_drain_source 和测试用的 finalize_and_drain 在收尾后会调用它,确保下一次流式输出不会混进上一轮残留内容。

调用图:被 3 处调用(finalize_and_drain, finalize_and_drain_source, clear)。

MarkdownStreamCollector::push_delta77–80 ↗
fn push_delta(&mut self, delta: &str)

作用:把新收到的一小段 Markdown 文本追加进缓存。这里的 delta 就是流式传输里刚到的一块文字。

数据流:输入一段字符串 delta → 它先记录一条 trace 级日志,方便调试,然后把这段文字接到内部 buffer 末尾 → 收集器里保存的原始 Markdown 变长。

调用关系:流式控制器或测试会反复调用它喂入新片段;喂完后通常会根据这段文字里有没有换行,继续调用 commit_complete_source 或测试用的 commit_complete_lines。

调用图:外部调用 1 个(trace!)。

MarkdownStreamCollector::commit_complete_source87–96 ↗
fn commit_complete_source(&mut self) -> Option<String>

作用:交出最新已经完整的原始 Markdown 行。它只认换行符,没遇到换行就不交,避免把没写完的一行提前显示。

数据流:读取内部 buffer 和上次已经提交到的位置 → 找到最后一个换行符,只截取“上次没交过、这次已经完整”的部分 → 返回这段新文本,并把已提交位置推进;如果没有新完整行,就返回 None。

调用关系:正式的流式显示流程会在收到含换行的片段后调用它;它不解析 Markdown,只给外部渲染器提供稳定的文本边界。

MarkdownStreamCollector::finalize_and_drain_source104–116 ↗
fn finalize_and_drain_source(&mut self) -> String

作用:在流真正结束时,把最后没交出去的原始 Markdown 也交出来。即使最后一行没有换行,它也会补一个换行,方便后续 Markdown 解析。

数据流:读取当前 buffer 和已提交位置 → 如果没有剩余内容,就清空并返回空字符串;如果还有剩余,就截出剩下的文本,必要时补上换行 → 清空收集器,返回最后这块文本。

调用关系:它是 commit_complete_source 的收尾搭档;流正常结束或中断时调用它,内部会调用 clear,避免旧内容污染下一次流。

调用图:调用 1 个内部函数(clear);外部调用 1 个(new)。

MarkdownStreamCollector::commit_complete_lines126–155 ↗
fn commit_complete_lines(&mut self) -> Vec<Line<'static>>

作用:这是测试专用函数:把已经完整的 Markdown 内容渲染成终端行,并只返回这次新增的那些行。它用来验证“分块收到”和“一次性收到”的显示是否一致。

数据流:读取缓存、宽度、测试当前目录和已提交行数 → 找到最后一个换行,把完整前缀交给 append_markdown 渲染,再去掉末尾可能多出来的纯空白行 → 返回上次之后新增的 Line,并更新已提交字节数和行数。

调用关系:simulate_stream_markdown_for_tests 和多项测试会用它模拟真实流式显示;它把具体 Markdown 渲染交给 markdown::append_markdown,并用 is_blank_line_spaces_only 判断末尾空白行。

调用图:调用 2 个内部函数(append_markdown, is_blank_line_spaces_only);外部调用 2 个(as_path, new)。

MarkdownStreamCollector::finalize_and_drain161–191 ↗
fn finalize_and_drain(&mut self) -> Vec<Line<'static>>

作用:这是测试专用的收尾函数:流结束时,把所有剩余 Markdown 渲染成终端行并返回。它确保最后没有换行的内容也能被测试看到。

数据流:复制当前缓存 → 如果为空就清空并返回空列表;如果末尾没有换行就临时补上 → 调用 append_markdown 渲染完整内容,只取之前没返回过的行 → 清空收集器,返回新增 Line。

调用关系:simulate_stream_markdown_for_tests 在 finalize 为 true 时调用它;它会记录 debug 和 trace 日志,把渲染交给 append_markdown,最后用 clear 重置状态。

调用图:调用 2 个内部函数(append_markdown, clear);外部调用 4 个(as_path, new, debug!, trace!)。

test_cwd195–199 ↗
fn test_cwd() -> PathBuf

作用:给测试提供一个稳定的当前目录路径。这样测试里涉及本地文件链接显示时,不会因为不同操作系统根目录不同而变得不稳定。

数据流:不接收输入 → 调用系统的临时目录函数 temp_dir → 返回一个 PathBuf 路径给测试使用。

调用关系:simulate_stream_markdown_for_tests 会调用它来创建 MarkdownStreamCollector;很多测试也通过它给收集器或 Markdown 渲染函数提供一致的目录。

调用图:被 1 处调用(simulate_stream_markdown_for_tests);外部调用 1 个(temp_dir)。

simulate_stream_markdown_for_tests202–218 ↗
fn simulate_stream_markdown_for_tests(
    deltas: &[&str],
    finalize: bool,
) -> Vec<Line<'static>>

作用:这是测试里的“假装流式输出”工具。它把一组小文本片段按顺序喂给收集器,模拟真实聊天内容一块块到达。

数据流:输入若干 delta 字符串和是否最终收尾的开关 → 创建收集器,逐个 push_delta;每当片段里有换行,就提交完整行;如果要求 finalize,就再交出最后剩余内容 → 输出渲染后的 Line 列表。

调用关系:许多端到端测试调用它来少写重复代码;它内部串起 new、test_cwd、push_delta、commit_complete_lines 和 finalize_and_drain,模拟完整的流式生命周期。

调用图:调用 2 个内部函数(new, test_cwd);被 6 处调用(assert_streamed_equals_full, empty_fenced_block_is_dropped_and_separator_preserved_before_heading, loose_list_with_split_dashes_matches_full_render, loose_vs_tight_list_items_streaming_matches_full, paragraph_then_empty_fence_then_heading_keeps_heading_on_new_line, utf8_boundary_safety_and_wide_chars);外部调用 1 个(new)。

tests::no_commit_until_newline226–234 ↗
async fn no_commit_until_newline()

作用:测试没有换行时不会提前提交内容。它防止半行文字被过早显示。

数据流:创建收集器 → 先放入没有换行的 “Hello, world”,检查没有输出;再放入 “!\n”,检查这时正好输出一行 → 测试通过说明换行边界生效。

调用关系:它直接使用 MarkdownStreamCollector::new、push_delta 和 commit_complete_lines,是最基础的边界行为测试。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert!, assert_eq!, test_cwd)。

tests::finalize_commits_partial_line237–242 ↗
async fn finalize_commits_partial_line()

作用:测试流结束时,即使最后一行没有换行,也会被输出。它保证结尾内容不会丢。

数据流:创建收集器 → 放入一行没有换行的文本 → 调用 finalize_and_drain → 检查返回一行内容。

调用关系:它验证测试收尾函数 finalize_and_drain 的价值,和 no_commit_until_newline 形成互补:平时不提前交,结束时必须交。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, test_cwd)。

tests::e2e_stream_blockquote_simple_is_green245–255 ↗
async fn e2e_stream_blockquote_simple_is_green()

作用:测试简单引用块在流式渲染后是绿色的。引用块就是 Markdown 里用 “>” 开头的文字。

数据流:输入 “> Hello\n” 到模拟流 → 得到渲染行 → 检查只有一行,并且前景色是绿色。

调用关系:它通过 simulate_stream_markdown_for_tests 走完整流式流程,重点确认收集器不会破坏引用块的样式。

调用图:外部调用 2 个(assert_eq!, simulate_stream_markdown_for_tests)。

tests::e2e_stream_blockquote_nested_is_green258–281 ↗
async fn e2e_stream_blockquote_nested_is_green()

作用:测试嵌套引用块也保持绿色。嵌套引用块就是 “>” 里面再有 “>>”。

数据流:把两行引用内容送进模拟流 → 过滤掉可能插入的空白引用行 → 检查剩下两行都显示为绿色。

调用关系:它依赖 simulate_stream_markdown_for_tests 生成流式结果,用来覆盖更复杂的引用层级。

调用图:外部调用 2 个(assert_eq!, simulate_stream_markdown_for_tests)。

tests::e2e_stream_blockquote_with_list_items_is_green284–292 ↗
async fn e2e_stream_blockquote_with_list_items_is_green()

作用:测试引用块里面放列表项时,整行仍然保持引用块的绿色样式。

数据流:输入两行 “> - item” 形式的 Markdown → 模拟流式渲染 → 检查输出两行,并且两行前景色都是绿色。

调用关系:它通过 simulate_stream_markdown_for_tests 验证引用和列表两种 Markdown 结构叠在一起时没有被收集器拆坏。

调用图:外部调用 2 个(assert_eq!, simulate_stream_markdown_for_tests)。

tests::e2e_stream_nested_mixed_lists_ordered_marker_is_light_blue295–323 ↗
async fn e2e_stream_nested_mixed_lists_ordered_marker_is_light_blue()

作用:测试多层混合列表里,第三层有序列表编号的颜色是浅蓝色。有序列表就是 “1.” 这种编号列表。

数据流:输入一段包含编号列表和项目符号列表的多层 Markdown → 模拟流式渲染 → 找到含有第三层编号文本的行 → 检查至少有一个片段是浅蓝色。

调用关系:它调用 simulate_stream_markdown_for_tests,确认流式提交不会让复杂列表的编号样式丢失。

调用图:外部调用 2 个(assert!, simulate_stream_markdown_for_tests)。

tests::e2e_stream_blockquote_wrap_preserves_green_style326–360 ↗
async fn e2e_stream_blockquote_wrap_preserves_green_style()

作用:测试长引用行被自动换行后,每一段仍然保持绿色。自动换行是终端宽度不够时把一行拆成多行显示。

数据流:输入一条很长的引用行 → 模拟流式渲染 → 再用 word_wrap_lines 按窄宽度强制换行 → 过滤空行后检查每个显示行的开头仍是绿色。

调用关系:它先用 simulate_stream_markdown_for_tests 得到结果,再把换行工作交给 wrapping::word_wrap_lines,检查渲染样式能穿过两个步骤保持不变。

调用图:调用 2 个内部函数(new, word_wrap_lines);外部调用 3 个(assert!, assert_eq!, simulate_stream_markdown_for_tests)。

tests::heading_starts_on_new_line_when_following_paragraph363–415 ↗
async fn heading_starts_on_new_line_when_following_paragraph()

作用:测试段落后面的标题会另起一行,而不是粘到前一句后面。标题就是 Markdown 里用 “##” 这类符号开头的行。

数据流:先输入 “Hello.\n” 并提交 → 检查只有段落行;再输入 “## Heading\n” 并提交 → 检查输出包含空白分隔行和标题行 → 最后确认原段落和标题文本正确。

调用关系:它直接操作收集器,覆盖标题紧跟段落时的增量提交行为,防止分块渲染把块级结构合并错。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, test_cwd)。

tests::heading_not_inlined_when_split_across_chunks418–489 ↗
async fn heading_not_inlined_when_split_across_chunks()

作用:测试标题被拆在不同数据块里时,也不会被错误地并进上一段文字。

数据流:先输入没有换行的段落,确认不提交;再输入换行加标题开头,先只提交完整段落;最后输入标题结尾换行,再提交标题 → 还额外用 append_markdown 检查普通整行渲染不会多出奇怪内容。

调用关系:它直接使用收集器和 markdown::append_markdown,专门验证“换行在一个块里、标题文字在后续块里”的真实流式场景。

调用图:调用 2 个内部函数(append_markdown, new);外部调用 4 个(new, assert!, assert_eq!, test_cwd)。

tests::lines_to_plain_strings491–502 ↗
fn lines_to_plain_strings(lines: &[ratatui::text::Line<'_>]) -> Vec<String>

作用:把终端渲染用的 Line 列表变成普通字符串列表,方便测试比较内容。Line 里可能有多个带样式的 span,这里只取文字。

数据流:输入一组 Line → 遍历每一行的 spans,把每个 span 的 content 拼起来 → 输出普通 String 列表。

调用关系:很多测试用它把复杂的带样式渲染结果简化成纯文本,再和预期结果做 assert_eq 比较。

调用图:外部调用 1 个(iter)。

tests::table_header_commits_without_holdback505–529 ↗
async fn table_header_commits_without_holdback()

作用:测试表格头、分隔线和表格内容在测试收集器里不会被无故延迟。这里的 holdback 指为了等更多内容而暂时不显示。

数据流:依次输入表头、分隔线、数据行和空行 → 每次提交并转成纯文本 → 检查表头能立即出现,后续行也持续有输出。

调用关系:它直接创建收集器,并用 lines_to_plain_strings 检查结果,确保这个文件的测试提交逻辑只按换行提交,不额外实现表格等待策略。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert!, assert_eq!, test_cwd, lines_to_plain_strings)。

tests::pipe_text_without_table_prefix_is_not_delayed532–538 ↗
async fn pipe_text_without_table_prefix_is_not_delayed()

作用:测试普通文字里出现竖线 “|” 时,不会被误当成表格而延迟显示。

数据流:输入一行包含多个竖线的普通句子 → 提交完整行 → 转成纯文本 → 检查输出就是原句。

调用关系:它使用收集器和 lines_to_plain_strings,验证换行边界逻辑不会因为看见表格常用符号而改变提交节奏。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, test_cwd, lines_to_plain_strings)。

tests::lists_and_fences_commit_without_duplication541–547 ↗
async fn lists_and_fences_commit_without_duplication()

作用:测试列表和围栏代码块在分块流式输入时不会重复显示。围栏代码块就是用三个反引号包起来的代码区域。

数据流:分别准备列表分块和代码块分块 → 调用 assert_streamed_equals_full → 比较流式渲染和一次性完整渲染是否完全一样。

调用关系:它把主要检查交给 assert_streamed_equals_full,是针对常见 Markdown 结构的回归测试。

调用图:外部调用 1 个(assert_streamed_equals_full)。

tests::utf8_boundary_safety_and_wide_chars550–582 ↗
async fn utf8_boundary_safety_and_wide_chars()

作用:测试表情、中文、控制字符、组合字符这些特殊 Unicode 文本在分块输入时不会被截坏或重复。Unicode 可以理解为统一表示各种语言和符号的字符标准。

数据流:准备包含 emoji、汉字和组合符号的完整文本,再把它拆成多个 delta → 用模拟流得到流式结果 → 同时用 append_markdown 做一次性完整渲染 → 把两边转成纯文本后比较完全相等。

调用关系:它调用 simulate_stream_markdown_for_tests、append_markdown 和 lines_to_plain_strings,重点保护多字节字符边界安全。

调用图:调用 2 个内部函数(append_markdown, simulate_stream_markdown_for_tests);外部调用 5 个(new, assert_eq!, test_cwd, lines_to_plain_strings, vec!)。

tests::e2e_stream_deep_nested_third_level_marker_is_light_blue585–631 ↗
async fn e2e_stream_deep_nested_third_level_marker_is_light_blue()

作用:更精确地测试深层列表里第三层编号标记是浅蓝色,而列表正文保持默认颜色。

数据流:输入一段多层列表 Markdown → 模拟流式渲染 → 找到第三层有序列表那一行 → 检查第一个 span,也就是缩进和 “1.” 标记,是浅蓝色;再检查正文 span 没有被染色。

调用关系:它使用 simulate_stream_markdown_for_tests 和 lines_to_plain_strings,细查样式分布,而不只是检查整行有没有某种颜色。

调用图:外部调用 4 个(assert!, assert_eq!, simulate_stream_markdown_for_tests, lines_to_plain_strings)。

tests::empty_fenced_block_is_dropped_and_separator_preserved_before_heading634–649 ↗
async fn empty_fenced_block_is_dropped_and_separator_preserved_before_heading()

作用:测试空代码块后面接标题时,空代码块标记不会显示出来,同时标题还能正常另起一行。

数据流:输入一个空的 fenced code block,再输入标题 → 模拟流式渲染并转成纯文本 → 检查结果里没有反引号代码块标记,并且能找到标题行。

调用关系:它通过 simulate_stream_markdown_for_tests 走流式路径,用 lines_to_plain_strings 做文本检查,覆盖空代码块这种容易产生多余行的情况。

调用图:调用 1 个内部函数(simulate_stream_markdown_for_tests);外部调用 3 个(assert!, lines_to_plain_strings, vec!)。

tests::paragraph_then_empty_fence_then_heading_keeps_heading_on_new_line652–668 ↗
async fn paragraph_then_empty_fence_then_heading_keeps_heading_on_new_line()

作用:测试段落、空代码块、标题连续出现时,标题不会和段落粘在一起。

数据流:依次输入段落、空代码块和标题 → 模拟流式渲染 → 转成纯文本 → 找到段落和标题的位置,检查标题出现在段落之后。

调用关系:它调用 simulate_stream_markdown_for_tests 和 lines_to_plain_strings;如果找不到关键行会 panic,说明渲染结构已经明显错了。

调用图:调用 1 个内部函数(simulate_stream_markdown_for_tests);外部调用 4 个(assert!, panic!, lines_to_plain_strings, vec!)。

tests::loose_list_with_split_dashes_matches_full_render671–694 ↗
async fn loose_list_with_split_dashes_matches_full_render()

作用:测试松散列表里单独拆开的短横线不会变成多余的悬空列表项。松散列表指列表项之间有空行的列表。

数据流:把 “- item” 后面接一个单独的 “-” 作为分块输入 → 得到流式渲染纯文本 → 再把所有分块拼成完整 Markdown 一次性渲染 → 比较两边完全一致。

调用关系:它同时使用 simulate_stream_markdown_for_tests、append_markdown 和 lines_to_plain_strings,是针对曾经发现的分块问题的定向回归测试。

调用图:调用 2 个内部函数(append_markdown, simulate_stream_markdown_for_tests);外部调用 5 个(new, assert_eq!, test_cwd, lines_to_plain_strings, vec!)。

tests::loose_vs_tight_list_items_streaming_matches_full697–801 ↗
async fn loose_vs_tight_list_items_streaming_matches_full()

作用:测试紧凑列表和松散列表混在一起时,流式输出的编号、缩进和空行都符合预期。紧凑列表是列表项之间没有空行,松散列表则有空行或段落。

数据流:用很多很小的 delta 模拟真实会话里的逐词输出 → 流式渲染并转成纯文本 → 同时做一次完整渲染用于诊断 → 最后和手写的预期行列表逐项比较。

调用关系:它调用 simulate_stream_markdown_for_tests、append_markdown 和 lines_to_plain_strings,覆盖复杂列表结构,也是比较综合的回归测试。

调用图:调用 2 个内部函数(append_markdown, simulate_stream_markdown_for_tests);外部调用 5 个(new, assert_eq!, test_cwd, lines_to_plain_strings, vec!)。

tests::assert_streamed_equals_full804–818 ↗
async fn assert_streamed_equals_full(deltas: &[&str])

作用:这是测试里的通用断言工具:检查分块流式渲染结果和一次性完整渲染结果完全一样。

数据流:输入一组 delta → 用 simulate_stream_markdown_for_tests 得到流式结果并转纯文本;再把 delta 拼成完整 Markdown,用 append_markdown 渲染并转纯文本 → 比较两边,不一样就带上完整输入内容报错。

调用关系:多个 fuzz 回归测试和结构测试都调用它;它把重复的“流式 vs 完整”比较流程集中到一个地方。

调用图:调用 2 个内部函数(append_markdown, simulate_stream_markdown_for_tests);外部调用 4 个(new, assert_eq!, test_cwd, lines_to_plain_strings)。

tests::fuzz_class_bullet_duplication_variant_1821–827 ↗
async fn fuzz_class_bullet_duplication_variant_1()

作用:测试一种由 fuzz 测试发现的项目符号重复问题。fuzz 测试就是自动生成很多奇怪输入来找 bug 的测试方法。

数据流:输入两段曾经会触发列表重复的 Markdown 分块 → 调用 assert_streamed_equals_full → 如果流式结果和完整渲染一致,就说明这个 bug 没复发。

调用关系:它把实际比较交给 assert_streamed_equals_full,是一个保留历史问题样本的回归测试。

调用图:外部调用 1 个(assert_streamed_equals_full)。

tests::fuzz_class_bullet_duplication_variant_2830–836 ↗
async fn fuzz_class_bullet_duplication_variant_2()

作用:测试另一种 fuzz 发现的项目符号重复场景,防止列表项被分块拆开后重复显示。

数据流:输入一组不同拆法的列表 Markdown 片段 → 调用 assert_streamed_equals_full → 比较流式和完整渲染。

调用关系:它和 fuzz_class_bullet_duplication_variant_1 一起覆盖同类问题的不同形态,核心检查仍交给 assert_streamed_equals_full。

调用图:外部调用 1 个(assert_streamed_equals_full)。

tests::streaming_html_block_then_text_matches_full839–846 ↗
async fn streaming_html_block_then_text_matches_full()

作用:测试 HTML 块后面接普通文字时,流式渲染和完整渲染一致。HTML 块就是 Markdown 里直接写的类似 <div> 的标签内容。

数据流:输入标题文字、HTML 行、普通文字三段 → 调用 assert_streamed_equals_full → 检查没有因为 HTML 块边界而多显示或少显示。

调用关系:它依赖 assert_streamed_equals_full,覆盖 Markdown 中 HTML 结构和普通文本相邻的场景。

调用图:外部调用 1 个(assert_streamed_equals_full)。

tests::table_like_lines_inside_fenced_code_are_not_held849–851 ↗
async fn table_like_lines_inside_fenced_code_are_not_held()

作用:测试代码块里的表格样式文本不会被当成真正表格而特殊等待。代码块里的内容应该按代码显示。

数据流:输入一个 fenced code block,其中间有 “| a | b |” 这种像表格的行 → 调用 assert_streamed_equals_full → 检查流式和完整渲染一致。

调用关系:它把比较交给 assert_streamed_equals_full,保护代码块内部文本不被外面的表格逻辑误判。

调用图:外部调用 1 个(assert_streamed_equals_full)。

tests::collector_source_chunks_round_trip_into_agent_fence_unwrapping854–888 ↗
async fn collector_source_chunks_round_trip_into_agent_fence_unwrapping()

作用:测试收集器交出的原始 Markdown 片段能正确送进 agent 专用渲染器,并触发 Markdown 代码围栏的解包逻辑。这里的 agent 渲染器是给助手输出用的特殊 Markdown 渲染入口。

数据流:创建收集器 → 分块输入一个 ```md 围起来的表格 → 每遇到换行就用 commit_complete_source 收集原始片段,最后用 finalize_and_drain_source 收尾 → 把完整原始文本交给 append_markdown_agent → 检查渲染结果里有表格分隔线,并且没有原始表头文本残留。

调用关系:它直接覆盖生产路径更关心的原始 source 提交流程:push_delta、commit_complete_source、finalize_and_drain_source 先合成文本,再交给 markdown::append_markdown_agent 做最终渲染。

调用图:调用 2 个内部函数(append_markdown_agent, new);外部调用 5 个(new, new, assert!, test_cwd, lines_to_plain_strings)。

tui/src/streaming/mod.rs源码 ↗
domain_logicmain loop

终端里的回答不是一次性出现的,而是一小段一小段流进来的。这个文件解决的就是“边来边显示,但不能乱序、不能一下子把界面撑爆”的问题。核心是 StreamState,它像一个小型候车队列:MarkdownStreamCollector 负责把零散 Markdown 内容收集和渲染成可显示的行;queued_lines 保存已经准备好、但还没真正放到屏幕上的行;每一行入队时还会记下时间,方便后面的策略判断“最老的一行等了多久”。这里特别强调先进先出,也就是先排队的先显示。文件还暴露了几个子模块,让更高层决定一次显示一行、显示多行,或者根据压力调整显示节奏。测试部分确认了一个重要细节:就算请求取很多行,也只会取当前队列里真正有的行,不会出错。

函数细节11
StreamState::new41–47 ↗
fn new(width: Option<usize>, cwd: &Path) -> Self

作用:创建一个新的流式输出状态。调用方会把当前工作目录传进来,这样 Markdown 里的本地文件链接可以按正确位置显示。

数据流:输入是可选的显示宽度和当前目录路径 → 它新建一个 Markdown 收集器、一个空的行队列,并把“是否见过增量内容”设为否 → 输出一个干净的 StreamState,准备接收后续流式文本。

调用关系:这是这套流式状态的起点。更高层创建流控制器时会用它;测试 drain_n_clamps_to_available_lines 也会先用它造出一个状态,再检查后续排队和取队列是否正常。

调用图:调用 1 个内部函数(new);被 2 处调用(new, drain_n_clamps_to_available_lines);外部调用 1 个(new)。

StreamState::clear49–53 ↗
fn clear(&mut self)

作用:把当前流的状态全部清空,准备迎接下一轮流式输出。它不只是清队列,也会清掉 Markdown 收集器里的临时内容。

数据流:输入是一个已有的 StreamState → 它清空 Markdown 收集器、清空等待显示的行队列,并把“见过增量内容”的标记改回否 → 这个状态回到刚开始那样的空白状态。

调用关系:当上层 reset 流程要重开一轮输出时会调用它。它负责做彻底复位,避免上一轮没显示完或没收集完的内容污染下一轮。

调用图:调用 1 个内部函数(clear);被 1 处调用(reset);外部调用 1 个(clear)。

StreamState::step55–61 ↗
fn step(&mut self) -> Vec<HyperlinkLine>

作用:从队列最前面取出一行,交给界面显示。它用于那种“一次推进一点点”的显示节奏。

数据流:输入是当前队列状态 → 它从队首弹出最早排队的一行,如果有就取出来,没有就什么也不取 → 输出一个列表,里面最多只有这一行,并且队列少了一行。

调用关系:上层的 tick 会调用它。tick 可以理解成界面刷新时的一次小动作,每次只放出一点内容,让显示看起来平滑且有秩序。

调用图:被 1 处调用(tick);外部调用 1 个(pop_front)。

StreamState::drain_n66–72 ↗
fn drain_n(&mut self, max_lines: usize) -> Vec<HyperlinkLine>

作用:一次从队列前面取出最多指定数量的行。它适合需要批量推进显示的时候,比如队列积压太多,需要多放几行。

数据流:输入是想取出的最大行数 max_lines 和当前队列 → 它先看队列实际有多少行,只取两者中较小的数量 → 输出这些被取出的行,并从队列里删除它们。

调用关系:上层的 tick_batch 会调用它做批量刷新。测试 drain_n_clamps_to_available_lines 专门验证:即使要求取 8 行,而队列只有 1 行,也只会安全地取出 1 行。

调用图:被 1 处调用(tick_batch);外部调用 2 个(drain, len)。

StreamState::clear_queue74–76 ↗
fn clear_queue(&mut self)

作用:只清空等待显示的行队列,但保留 Markdown 收集器和当前流生命周期的其他状态。它用于“显示队列要重建,但整轮流还没结束”的场景。

数据流:输入是当前 StreamState → 它只把 queued_lines 清空 → 输出没有返回值,但状态里的待显示队列变成空,其他部分保持原样。

调用关系:当上层需要重建稳定显示队列、切换渲染模式、调整宽度或同步队列时会用它。这样可以先倒掉旧的排队结果,再按新规则重新装入内容。

调用图:被 4 处调用(rebuild_stable_queue_from_render, set_render_mode, set_width, sync_stable_queue);外部调用 1 个(clear)。

StreamState::is_idle78–80 ↗
fn is_idle(&self) -> bool

作用:判断现在有没有排队等待显示的行。简单说,就是问这条流的显示队列是不是空了。

数据流:输入是当前 StreamState → 它检查 queued_lines 是否为空 → 输出 true 或 false,true 表示没有待显示内容。

调用关系:上层也有自己的 is_idle,会通过它判断底层队列是否已经清空。这个判断常用于决定要不要继续刷新、提交或等待。

调用图:被 1 处调用(is_idle);外部调用 1 个(is_empty)。

StreamState::queued_len82–84 ↗
fn queued_len(&self) -> usize

作用:返回当前还有多少行在排队等待显示。它让上层知道队列压力有多大。

数据流:输入是当前 StreamState → 它读取 queued_lines 的长度 → 输出一个数字,表示待显示行数,不改动任何内容。

调用关系:上层的 queued_lines、set_render_mode、set_width 和 sync_stable_queue 会用到它。比如改宽度或改显示模式时,需要知道旧队列里还有多少东西,才能决定怎么同步。

调用图:被 4 处调用(queued_lines, set_render_mode, set_width, sync_stable_queue);外部调用 1 个(len)。

StreamState::oldest_queued_age86–90 ↗
fn oldest_queued_age(&self, now: Instant) -> Option<Duration>

作用:查看队列里最早那一行已经等了多久。这个信息可以帮助上层判断是否该加快显示,避免内容一直憋在队列里。

数据流:输入是当前时间 now 和队列状态 → 它看队首那一行的入队时间,并计算从入队到现在经过了多久;如果队列为空就没有结果 → 输出一个可选的时间长度。

调用关系:上层的 oldest_queued_age 会调用它。它不需要偷看文字内容,只看最老一行的等待时间,这样策略代码能根据“等了多久”做决定。

调用图:被 1 处调用(oldest_queued_age);外部调用 1 个(front)。

StreamState::enqueue92–99 ↗
fn enqueue(&mut self, lines: Vec<HyperlinkLine>)

作用:把一批已经整理好的显示行放进队列末尾。它保证新来的内容排在旧内容后面,不会插队。

数据流:输入是一组 HyperlinkLine,也就是可能带终端超链接的显示行 → 它记录当前时间,并给这一批行都贴上同一个入队时间,再追加到队列尾部 → 输出没有返回值,但队列多了这些待显示行。

调用关系:rebuild_stable_queue_from_render 和 sync_stable_queue 会调用它,把重新渲染或同步出来的稳定行放回队列。后续 step 或 drain_n 再按先进先出的顺序取走。

调用图:被 2 处调用(rebuild_stable_queue_from_render, sync_stable_queue);外部调用 2 个(now, extend)。

tests::test_cwd109–113 ↗
fn test_cwd() -> PathBuf

作用:给测试准备一个稳定的当前目录路径。它用系统临时目录,避免测试依赖某个操作系统特有的根目录写法。

数据流:没有业务输入 → 它向系统询问临时目录在哪里 → 输出一个 PathBuf 路径,供测试创建 StreamState 时使用。

调用关系:测试 drain_n_clamps_to_available_lines 会调用它。它只是测试辅助工具,让测试里的路径参数合法且跨平台。

调用图:外部调用 1 个(temp_dir)。

tests::drain_n_clamps_to_available_lines116–123 ↗
fn drain_n_clamps_to_available_lines()

作用:验证 drain_n 的安全行为:要求取出的行数超过队列现有数量时,不会出错,也不会凭空产生内容。

数据流:它先创建一个 StreamState,并放入一行文字 one → 然后要求一次取最多 8 行 → 结果应该只得到那 1 行,并且队列随后变为空。

调用关系:这是针对 StreamState::drain_n 的单元测试。它会用 StreamState::new 创建状态,用 tests::test_cwd 提供目录,再用断言确认取出的内容和空队列状态都符合预期。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert!, assert_eq!, test_cwd, vec!)。

tui/src/streaming/controller.rs源码 ↗
domain_logicmain loop / streaming render / resize handling / tests

模型回答不是一次性出来的,而是一小段一小段流出来的。这个文件把正在流出的内容分成两块:稳定区和尾巴区。稳定区像已经贴进聊天记录的纸条,不应该再改;尾巴区像还在草稿纸上写的最后几行,可以随时重排。这样用户能马上看到新内容,又不会因为 Markdown 表格、自动换行、终端变宽变窄而看到内容跳来跳去。核心零件是 StreamCore,它保存原始文字、重新渲染后的行、待提交队列和表格延后判断。StreamController 给普通助手消息套上消息样式;PlanStreamController 给“计划”内容加标题、缩进和背景样式。文件里还带了大量测试,专门防止 resize、表格、代码块、链接等场景出现重复、丢行或错误渲染。

函数细节103
StreamCore::new107–120 ↗
fn new(width: Option<usize>, cwd: &Path, render_mode: HistoryRenderMode) -> Self

作用:创建一个流式输出的核心记录本。它准备好保存原文、渲染结果、提交队列、当前宽度和表格检测状态。

数据流:输入终端内容宽度、当前工作目录和渲染模式 → 建好 StreamState、空字符串、空行列表、缓存和表格扫描器 → 返回一个还没接收任何内容的 StreamCore。

调用关系:普通消息控制器和计划控制器在创建时都会先调用它,把共用的流式处理底座搭起来。

调用图:调用 2 个内部函数(new, new);被 2 处调用(new, new);外部调用 3 个(to_path_buf, with_capacity, with_capacity)。

StreamCore::push_delta129–145 ↗
fn push_delta(&mut self, delta: &str) -> bool

作用:接收模型新吐出来的一小段文字,并决定哪些行已经安全到可以放进聊天记录。它会等到一行真正结束后再提交,避免半行表格闪一下又变掉。

数据流:输入一段 delta 文本 → 放进收集器;如果里面有换行,就取出完整来源、追加到原文、更新表格扫描、重新渲染并同步稳定队列 → 返回这次有没有新稳定行入队。

调用关系:StreamController::push 和 PlanStreamController::push 都把新内容交给它;它再调用重新渲染和队列同步这两步。

调用图:调用 3 个内部函数(recompute_streaming_render, sync_stable_queue, push_source_chunk);被 2 处调用(push, push)。

StreamCore::finalize_remaining154–166 ↗
fn finalize_remaining(&mut self) -> Vec<HyperlinkLine>

作用:在一次流式输出结束时,把还没显示完的尾巴一次性整理出来。它保证最终聊天记录以完整原文重新渲染为准。

数据流:读取收集器里剩下的未完成文字 → 追加到原文并更新表格扫描 → 用完整原文重新渲染 → 返回尚未发到聊天记录的那些行。

调用关系:普通消息和计划在 finalize 时都会调用它;之后外层控制器会把这些行包装成对应的历史单元。

调用图:调用 2 个内部函数(render_source, push_source_chunk);被 2 处调用(finalize, finalize);外部调用 1 个(new)。

StreamCore::tick169–173 ↗
fn tick(&mut self) -> Vec<HyperlinkLine>

作用:推进一次提交动画,也就是从稳定队列里拿出一小步内容显示。它同时记住已经真正显示了多少行。

数据流:读取当前队列 → 从 StreamState 取出一步要显示的行 → 增加已发出行数 → 返回这一步的行。

调用关系:两个控制器的 on_commit_tick 都调用它,用来一点点把稳定内容搬到聊天记录。

调用图:调用 1 个内部函数(step);被 2 处调用(on_commit_tick, on_commit_tick)。

StreamCore::tick_batch176–186 ↗
fn tick_batch(&mut self, max_lines: usize) -> Vec<HyperlinkLine>

作用:一次性从稳定队列里取出最多指定数量的行。它用于想快速清空或批量推进动画的场景。

数据流:输入最多取几行 → 如果是 0 就什么也不做;否则从队列取出这些行 → 更新已发出行数 → 返回取出的行。

调用关系:两个控制器的 on_commit_tick_batch 调用它;测试里也用它验证 resize 后队列能被完整排出。

调用图:调用 1 个内部函数(drain_n);被 2 处调用(on_commit_tick_batch, on_commit_tick_batch);外部调用 1 个(new)。

StreamCore::is_idle192–194 ↗
fn is_idle(&self) -> bool

作用:判断当前稳定队列是不是已经空了。外层用它知道这轮动画是否结束。

数据流:读取 StreamState 的队列状态 → 转交给 StreamState 判断 → 返回是否空闲。

调用关系:每次普通消息或计划消息提交一批内容后,外层都会用它决定是否继续 tick。

调用图:调用 1 个内部函数(is_idle);被 4 处调用(on_commit_tick, on_commit_tick_batch, on_commit_tick, on_commit_tick_batch)。

StreamCore::queued_lines197–199 ↗
fn queued_lines(&self) -> usize

作用:告诉外面现在还有多少稳定行排队等显示。它主要用于节流、动画和测试检查。

数据流:读取 StreamState 队列长度 → 返回待显示行数。

调用关系:两个控制器都把这个信息暴露出去,调用方可据此决定是否需要继续刷新。

调用图:调用 1 个内部函数(queued_len);被 2 处调用(queued_lines, queued_lines)。

StreamCore::oldest_queued_age202–204 ↗
fn oldest_queued_age(&self, now: Instant) -> Option<Duration>

作用:查看队列里最早那批内容已经等了多久。这样上层可以判断动画是不是积压太久。

数据流:输入当前时间 → 询问 StreamState 最老排队项的年龄 → 返回一个时长,或者没有队列时返回空。

调用关系:两个控制器提供同名接口,把排队老化信息交给外层调度逻辑。

调用图:调用 1 个内部函数(oldest_queued_age);被 2 处调用(oldest_queued_age, oldest_queued_age)。

StreamCore::current_tail_lines214–217 ↗
fn current_tail_lines(&self) -> Vec<HyperlinkLine>

作用:取出还不能正式写入聊天记录、但可以作为临时尾巴显示的行。它避免已入队但还没播放的行在尾巴里重复出现。

数据流:读取当前渲染行和已入队稳定边界 → 从边界之后切出尾巴 → 返回这些可变行。

调用关系:普通消息和计划消息都用它显示活动单元里的实时尾巴。

调用图:被 2 处调用(current_tail_lines, current_tail_lines)。

StreamCore::has_tail220–222 ↗
fn has_tail(&self) -> bool

作用:判断当前有没有还在变化的尾巴内容。它告诉外层是否需要保留一个临时显示区域。

数据流:比较已入队稳定行数和总渲染行数 → 如果后面还有内容就返回 true。

调用关系:两个控制器的 has_live_tail 会调用它;改宽度和改渲染模式时也用它保护未稳定尾巴。

调用图:被 4 处调用(has_live_tail, has_live_tail, set_render_mode, set_width)。

StreamCore::set_width233–265 ↗
fn set_width(&mut self, width: Option<usize>)

作用:终端宽度变化时重新排版正在流的内容。它尽量不重放已经显示过的内容,同时保住还没显示的队列和可变尾巴。

数据流:输入新宽度 → 如果变了,就更新收集器宽度、重新渲染原文、修正已显示计数、清掉旧宽度下的队列并重建 → 改动内部排版状态。

调用关系:两个外层控制器的 set_width 都转给它;它会调用重新渲染和队列重建,防止 resize 后丢行或重复。

调用图:调用 5 个内部函数(clear_queue, queued_len, has_tail, rebuild_stable_queue_from_render, recompute_streaming_render);被 2 处调用(set_width, set_width)。

StreamCore::reset268–276 ↗
fn reset(&mut self)

作用:清空一次流式输出留下的所有临时状态。没有它,上一条回答可能会混进下一条回答。

数据流:读取并清理内部状态 → 清空队列、原文、渲染行、计数、缓存和表格扫描器 → 回到刚创建后的干净状态。

调用关系:两个 finalize 在拿走最终结果后都会调用它,作为收尾清场。

调用图:调用 2 个内部函数(clear, reset);被 2 处调用(finalize, finalize)。

StreamCore::render_source278–287 ↗
fn render_source(&self, source: &str) -> Vec<HyperlinkLine>

作用:把原始 Markdown 文本变成终端能显示的行。它支持富文本模式和原样文本模式。

数据流:输入源文本 → 如果是 Rich 就按 Markdown、链接和当前目录渲染;如果是 Raw 就按原始行处理 → 返回带链接信息的显示行。

调用关系:最终渲染和每次重新渲染都靠它,是原文到屏幕行之间的转换器。

调用图:调用 3 个内部函数(raw_lines_from_source, render_markdown_agent_with_links_and_cwd, plain_hyperlink_lines);被 2 处调用(finalize_remaining, recompute_streaming_render);外部调用 1 个(as_path)。

StreamCore::recompute_streaming_render289–291 ↗
fn recompute_streaming_render(&mut self)

作用:用当前原文和当前设置重新生成整份渲染快照。它让后续稳定区和尾巴都基于最新画面计算。

数据流:读取 raw_source、宽度和渲染模式 → 调用 render_source → 覆盖 rendered_lines。

调用关系:收到完整新行、窗口改宽、渲染模式改变时都会调用它。

调用图:调用 1 个内部函数(render_source);被 3 处调用(push_delta, set_render_mode, set_width)。

StreamCore::set_render_mode293–319 ↗
fn set_render_mode(&mut self, render_mode: HistoryRenderMode)

作用:切换渲染模式,比如从富 Markdown 显示切到原始文本显示。它会像 resize 一样重新整理队列,避免画面错位。

数据流:输入新模式 → 如果真的改变,就记住旧队列和尾巴状态、重新渲染、修正计数、清队列并按新模式重建 → 内部显示状态更新。

调用关系:两个外层控制器的 set_render_mode 会调用它;它复用 resize 类似的保护逻辑。

调用图:调用 5 个内部函数(clear_queue, queued_len, has_tail, rebuild_stable_queue_from_render, recompute_streaming_render);被 2 处调用(set_render_mode, set_render_mode)。

StreamCore::compute_target_stable_len322–328 ↗
fn compute_target_stable_len(&mut self) -> usize

作用:计算当前有多少渲染行可以算作稳定区。它是决定“哪些能进聊天记录、哪些还要留在尾巴”的核心判断。

数据流:读取总渲染行数和已发出行数 → 计算需要扣住的尾巴行数 → 得出目标稳定行数,且不会小于已发出的行数。

调用关系:同步队列和重建队列时都会先问它稳定边界在哪里。

调用图:调用 1 个内部函数(active_tail_budget_lines);被 2 处调用(rebuild_stable_queue_from_render, sync_stable_queue)。

StreamCore::sync_stable_queue332–356 ↗
fn sync_stable_queue(&mut self) -> bool

作用:把新变稳定的行加入提交队列。遇到表格导致稳定边界回退时,它会重建队列而不是硬接旧内容。

数据流:计算目标稳定行数 → 如果边界后退就清队列并按新快照重放未发稳定行;如果前进就把新增稳定行入队 → 返回是否有新队列内容。

调用关系:push_delta 在每次完整行到达并重新渲染后调用它。

调用图:调用 4 个内部函数(clear_queue, enqueue, queued_len, compute_target_stable_len);被 1 处调用(push_delta)。

StreamCore::rebuild_stable_queue_from_render363–371 ↗
fn rebuild_stable_queue_from_render(&mut self)

作用:按当前渲染快照重新生成稳定队列。它主要在宽度或渲染模式改变后使用,因为旧队列的换行已经不可信。

数据流:计算目标稳定行数 → 清空旧队列 → 把已显示之后、稳定边界之前的行重新入队 → 更新稳定边界。

调用关系:set_width 和 set_render_mode 在重新渲染后调用它。

调用图:调用 3 个内部函数(clear_queue, enqueue, compute_target_stable_len);被 2 处调用(set_render_mode, set_width)。

StreamCore::active_tail_budget_lines381–401 ↗
fn active_tail_budget_lines(&mut self) -> usize

作用:决定现在要扣住多少行作为可变尾巴。尤其是表格出现时,它会把表格从表头开始留在尾巴里,等最终确定再提交。

数据流:读取表格扫描器状态 → 如果发现或怀疑表格,就把源文本位置换算成尾巴行数;否则尾巴预算为 0 → 返回应扣住的行数。

调用关系:compute_target_stable_len 调用它;它再根据扫描器状态转给 tail_budget_from_source_start。

调用图:调用 2 个内部函数(tail_budget_from_source_start, state);被 1 处调用(compute_target_stable_len);外部调用 2 个(now, trace!)。

StreamCore::tail_budget_from_source_start408–415 ↗
fn tail_budget_from_source_start(&mut self, source_start: usize) -> usize

作用:把“原文里的某个字节位置”转换成“渲染结果里要扣住多少行”。这是原文坐标和屏幕行坐标之间的桥。

数据流:输入表格开始的源文本位置 → 算出这个位置之前会渲染成多少行 → 用总渲染行数减去前缀行数 → 返回尾巴行数。

调用关系:active_tail_budget_lines 在发现表格或疑似表头时调用它。

调用图:调用 1 个内部函数(stable_prefix_len_for_source_start);被 1 处调用(active_tail_budget_lines)。

StreamCore::stable_prefix_len_for_source_start422–456 ↗
fn stable_prefix_len_for_source_start(&mut self, source_start: usize) -> usize

作用:计算表格开始前的那段原文会渲染成几行,并缓存结果。缓存能避免表格一行行到来时反复做同样的渲染。

数据流:输入源文本位置 → 如果缓存的起点和宽度匹配就直接返回;否则渲染前缀文本、数行数、写入缓存 → 返回前缀行数。

调用关系:tail_budget_from_source_start 调用它;它直接调用 Markdown 渲染器来得到准确行数。

调用图:调用 1 个内部函数(render_markdown_agent_with_links_and_cwd);被 1 处调用(tail_budget_from_source_start);外部调用 3 个(now, as_path, trace!)。

StreamController::new473–478 ↗
fn new(width: Option<usize>, cwd: &Path, render_mode: HistoryRenderMode) -> Self

作用:创建普通助手消息的流式控制器。它在 StreamCore 之外再记录消息头是否已经显示。

数据流:输入宽度、当前目录和渲染模式 → 创建 StreamCore,并把 header_emitted 设为 false → 返回普通消息控制器。

调用关系:上层收到助手流式回答时会创建它;内部依赖 StreamCore::new。

调用图:调用 1 个内部函数(new);被 4 处调用(handle_streaming_delta, flush_answer_stream_keeps_default_reflow_for_plain_text_tail, flush_answer_stream_requests_scrollback_reflow_for_live_table_tail, stream_controller)。

StreamController::push480–482 ↗
fn push(&mut self, delta: &str) -> bool

作用:把普通助手消息的新片段送进流式系统。调用者不用关心表格和队列细节。

数据流:输入 delta → 直接交给 core.push_delta → 返回是否有稳定行入队。

调用关系:这是普通消息外层入口,实际工作由 StreamCore::push_delta 完成。

调用图:调用 1 个内部函数(push_delta)。

StreamController::finalize486–498 ↗
fn finalize(&mut self) -> (Option<Box<dyn HistoryCell>>, Option<String>)

作用:结束普通助手消息流,并拿到最后要写入历史的单元和原始 Markdown。原文会用于后续整理成正式聊天记录。

数据流:调用 core.finalize_remaining 得到剩余行 → 如果没有原文就重置并返回空;否则取走原文、emit 成消息单元、重置状态 → 返回单元和原文。

调用关系:一次助手回答结束时调用;它把核心结果交给 emit 包装成 AgentMessageCell。

调用图:调用 3 个内部函数(emit, finalize_remaining, reset);外部调用 1 个(take)。

StreamController::on_commit_tick500–503 ↗
fn on_commit_tick(&mut self) -> (Option<Box<dyn HistoryCell>>, bool)

作用:普通消息每次动画 tick 时取一小步稳定内容显示。它返回这步的历史单元和队列是否空了。

数据流:从 core.tick 取行 → 用 emit 包装 → 查询 core.is_idle → 返回可显示单元和空闲状态。

调用关系:drain_stream_controller 等外层排队刷新逻辑会调用它。

调用图:调用 3 个内部函数(emit, is_idle, tick);被 1 处调用(drain_stream_controller)。

StreamController::on_commit_tick_batch505–511 ↗
fn on_commit_tick_batch(
        &mut self,
        max_lines: usize,
    ) -> (Option<Box<dyn HistoryCell>>, bool)

作用:普通消息一次取多行稳定内容显示。适合需要快速排空队列的时候。

数据流:输入最多行数 → core.tick_batch 取出这些行 → emit 包装 → 返回单元和是否空闲。

调用关系:批量排空普通消息流时由 drain_stream_controller 调用。

调用图:调用 3 个内部函数(emit, is_idle, tick_batch);被 1 处调用(drain_stream_controller)。

StreamController::queued_lines517–519 ↗
fn queued_lines(&self) -> usize

作用:查看普通消息还有多少稳定行等待提交。用于判断动画积压情况。

数据流:读取 core.queued_lines → 返回行数。

调用关系:它只是 StreamCore 对外的一层普通消息接口。

调用图:调用 1 个内部函数(queued_lines)。

StreamController::oldest_queued_age521–523 ↗
fn oldest_queued_age(&self, now: Instant) -> Option<Duration>

作用:查看普通消息队列里最早内容等了多久。上层可以用它决定是否加速刷新。

数据流:输入当前时间 → 转给 core.oldest_queued_age → 返回等待时长或空。

调用关系:它把 StreamCore 的队列年龄信息暴露给普通消息调度。

调用图:调用 1 个内部函数(oldest_queued_age)。

StreamController::current_tail_lines526–528 ↗
fn current_tail_lines(&self) -> Vec<HyperlinkLine>

作用:取得普通消息当前临时尾巴的原始显示行。外层可以把它放在活动单元里显示。

数据流:无额外输入 → 从 core.current_tail_lines 取出未入队尾巴 → 返回这些行。

调用关系:UI 渲染普通助手正在输出的尾部时会用它。

调用图:调用 1 个内部函数(current_tail_lines)。

StreamController::tail_starts_stream531–533 ↗
fn tail_starts_stream(&self) -> bool

作用:判断当前尾巴是不是从整条消息开头就开始。这样 UI 能决定是否还需要显示消息头。

数据流:读取 header_emitted 和 core.enqueued_stable_len → 如果头没发且稳定区为空就返回 true。

调用关系:活动尾巴显示时用它判断尾巴是不是第一块内容。

StreamController::has_live_tail536–538 ↗
fn has_live_tail(&self) -> bool

作用:判断普通消息有没有正在变化的尾巴。比如表格正在生成时通常会有。

数据流:调用 core.has_tail → 返回是否存在尾巴。

调用关系:上层用它决定是否显示临时流尾单元。

调用图:调用 1 个内部函数(has_tail)。

StreamController::clear_queue540–543 ↗
fn clear_queue(&mut self)

作用:清掉普通消息当前等待提交的稳定队列。它还把已入队边界拉回到已发出位置,避免状态不一致。

数据流:清空 core.state 队列 → 把 enqueued_stable_len 设为 emitted_stable_len → 内部队列被丢弃。

调用关系:外层需要取消或重排队列时会用它。

StreamController::set_width545–547 ↗
fn set_width(&mut self, width: Option<usize>)

作用:通知普通消息控制器终端内容宽度变了。它会触发核心重新排版。

数据流:输入新宽度 → 转给 core.set_width → 内部渲染和队列按新宽度更新。

调用关系:窗口 resize 后由上层调用;实际复杂处理在 StreamCore::set_width。

调用图:调用 1 个内部函数(set_width)。

StreamController::set_render_mode549–551 ↗
fn set_render_mode(&mut self, render_mode: HistoryRenderMode)

作用:切换普通消息的显示方式,比如富文本和原始文本。它让核心重新渲染当前流。

数据流:输入新渲染模式 → 转给 core.set_render_mode → 内部队列和尾巴随之重建。

调用关系:设置变化时由上层调用;具体重排由 StreamCore 完成。

调用图:调用 1 个内部函数(set_render_mode)。

StreamController::emit553–564 ↗
fn emit(&mut self, lines: Vec<HyperlinkLine>) -> Option<Box<dyn HistoryCell>>

作用:把普通助手消息的一组显示行包装成历史单元。第一次输出时会带上消息头,之后就不重复带头。

数据流:输入若干 HyperlinkLine → 如果为空返回空;否则创建 AgentMessageCell,并更新 header_emitted → 返回可插入历史的单元。

调用关系:finalize、on_commit_tick 和 on_commit_tick_batch 都用它把核心行变成 UI 能放进聊天记录的东西。

调用图:调用 1 个内部函数(new_hyperlink_lines);被 3 处调用(finalize, on_commit_tick, on_commit_tick_batch);外部调用 1 个(new)。

PlanStreamController::new586–592 ↗
fn new(width: Option<usize>, cwd: &Path, render_mode: HistoryRenderMode) -> Self

作用:创建“提议计划”的流式控制器。它除了核心流式状态,还记录计划标题和顶部空行是否已经显示。

数据流:输入宽度、当前目录和渲染模式 → 创建 StreamCore,并把 header_emitted、top_padding_emitted 设为 false → 返回计划控制器。

调用关系:上层收到计划流时创建它;底层仍使用 StreamCore::new。

调用图:调用 1 个内部函数(new);被 5 处调用(on_plan_delta, completed_token_activity_refresh_retries_after_plan_item_completion, completed_plan_table_tail_skips_provisional_history_insert, finalized_plan_stream_preserves_semantic_url_fragments, plan_stream_controller)。

PlanStreamController::push594–596 ↗
fn push(&mut self, delta: &str) -> bool

作用:把计划内容的新片段送进流式系统。它复用普通消息一样的稳定区和尾巴机制。

数据流:输入 delta → 交给 core.push_delta → 返回是否有稳定行入队。

调用关系:计划流的外层入口,真正的增量处理由 StreamCore 负责。

调用图:调用 1 个内部函数(push_delta)。

PlanStreamController::finalize600–612 ↗
fn finalize(&mut self) -> (Option<Box<dyn HistoryCell>>, Option<String>)

作用:结束计划流,返回最终计划单元和原始 Markdown。它会在最后补上计划块底部留白。

数据流:调用 core.finalize_remaining → 如果没有原文就重置并返回空;否则取走原文、emit 剩余行且包含底部空行、重置 → 返回单元和原文。

调用关系:计划输出结束时调用;它把核心结果交给计划专用 emit 包装。

调用图:调用 3 个内部函数(emit, finalize_remaining, reset);外部调用 1 个(take)。

PlanStreamController::on_commit_tick614–620 ↗
fn on_commit_tick(&mut self) -> (Option<Box<dyn HistoryCell>>, bool)

作用:计划流每次动画 tick 时提交一小步稳定内容。中途提交不会加底部留白。

数据流:从 core.tick 取一批行 → 用计划 emit 包装且不带底部 padding → 查询是否空闲 → 返回单元和状态。

调用关系:drain_plan_stream_controller 会在计划流播放队列时调用它。

调用图:调用 3 个内部函数(emit, is_idle, tick);被 1 处调用(drain_plan_stream_controller)。

PlanStreamController::on_commit_tick_batch622–631 ↗
fn on_commit_tick_batch(
        &mut self,
        max_lines: usize,
    ) -> (Option<Box<dyn HistoryCell>>, bool)

作用:计划流一次提交多行稳定内容。它适合快速排空计划队列。

数据流:输入最多行数 → core.tick_batch 取行 → emit 为计划单元且不带底部留白 → 返回单元和是否空闲。

调用关系:批量排空计划流时由 drain_plan_stream_controller 调用。

调用图:调用 3 个内部函数(emit, is_idle, tick_batch);被 1 处调用(drain_plan_stream_controller)。

PlanStreamController::queued_lines634–636 ↗
fn queued_lines(&self) -> usize

作用:查看计划流还有多少稳定行没显示。它用于调度和测试。

数据流:读取 core.queued_lines → 返回待显示行数。

调用关系:这是计划控制器对 StreamCore 队列长度的简单转发。

调用图:调用 1 个内部函数(queued_lines)。

PlanStreamController::has_live_tail639–641 ↗
fn has_live_tail(&self) -> bool

作用:判断计划内容是否有正在变化的尾巴。表格计划常会被留在尾巴直到最终确定。

数据流:调用 core.has_tail → 返回是否有可变尾巴。

调用关系:上层决定是否显示计划临时尾巴时会用它。

调用图:调用 1 个内部函数(has_tail)。

PlanStreamController::current_tail_lines644–646 ↗
fn current_tail_lines(&self) -> Vec<HyperlinkLine>

作用:取出计划流的未稳定尾巴行,未加计划块外观。它是计划尾巴显示的原材料。

数据流:从 core.current_tail_lines 读取 → 返回尾巴行。

调用关系:current_tail_display_lines 会在它基础上加计划样式。

调用图:调用 1 个内部函数(current_tail_lines);被 1 处调用(current_tail_display_lines)。

PlanStreamController::tail_starts_stream649–651 ↗
fn tail_starts_stream(&self) -> bool

作用:判断计划尾巴是否从计划块开头开始。这样 UI 能决定是否要连同标题一起显示。

数据流:读取 header_emitted 和 core.enqueued_stable_len → 如果标题未发且稳定边界为 0,就返回 true。

调用关系:计划活动尾巴渲染时可用它判断显示形态。

PlanStreamController::current_tail_display_lines653–659 ↗
fn current_tail_display_lines(&self) -> Vec<HyperlinkLine>

作用:取得已经加好计划标题、缩进和样式的尾巴行。它给 UI 直接显示临时计划块用。

数据流:先取 current_tail_lines → 如果为空返回空;否则调用 render_display_lines 加外观 → 返回可显示行。

调用关系:它连接核心尾巴数据和计划专用展示格式。

调用图:调用 2 个内部函数(current_tail_lines, render_display_lines);外部调用 1 个(new)。

PlanStreamController::oldest_queued_age661–663 ↗
fn oldest_queued_age(&self, now: Instant) -> Option<Duration>

作用:查看计划稳定队列中最早内容等了多久。用于判断计划动画是否积压。

数据流:输入当前时间 → 转给 core.oldest_queued_age → 返回时长或空。

调用关系:计划流调度可以用它和普通消息一样做节奏控制。

调用图:调用 1 个内部函数(oldest_queued_age)。

PlanStreamController::clear_queue665–668 ↗
fn clear_queue(&mut self)

作用:清空计划流当前等待提交的稳定队列。它同步修正入队边界,避免之后重复显示。

数据流:清空 core.state 队列 → 把 enqueued_stable_len 改成 emitted_stable_len → 队列被重置。

调用关系:外层需要取消或重排计划流时会用它。

PlanStreamController::set_width670–672 ↗
fn set_width(&mut self, width: Option<usize>)

作用:通知计划控制器内容宽度变了。计划正文会按新宽度重新排版。

数据流:输入新宽度 → 调用 core.set_width → 内部渲染快照和队列被更新。

调用关系:终端 resize 后由上层调用,核心处理和普通消息相同。

调用图:调用 1 个内部函数(set_width)。

PlanStreamController::set_render_mode674–676 ↗
fn set_render_mode(&mut self, render_mode: HistoryRenderMode)

作用:切换计划内容的渲染方式。它让计划流在新模式下保持队列和尾巴正确。

数据流:输入新渲染模式 → 转给 core.set_render_mode → 内部重新渲染并重建必要队列。

调用关系:设置变化时由上层调用,实际重排由 StreamCore 完成。

调用图:调用 1 个内部函数(set_render_mode)。

PlanStreamController::emit678–696 ↗
fn emit(
        &mut self,
        lines: Vec<HyperlinkLine>,
        include_bottom_padding: bool,
    ) -> Option<Box<dyn HistoryCell>>

作用:把计划内容行包装成一个计划历史单元。它负责决定是不是续块,以及要不要加底部空行。

数据流:输入显示行和是否包含底部留白 → 如果没有行且不需要留白就返回空;否则渲染计划外观、更新标题和顶部留白状态 → 返回计划单元。

调用关系:计划 finalize 和提交 tick 都通过它生成 new_proposed_plan_stream。

调用图:调用 1 个内部函数(render_display_lines);被 3 处调用(finalize, on_commit_tick, on_commit_tick_batch);外部调用 2 个(new, new_proposed_plan_stream)。

PlanStreamController::render_display_lines698–727 ↗
fn render_display_lines(
        &self,
        lines: Vec<HyperlinkLine>,
        include_bottom_padding: bool,
    ) -> Vec<HyperlinkLine>

作用:给计划正文加上“Proposed Plan”标题、空行、缩进和背景样式。它把普通渲染行变成计划块外观。

数据流:输入正文行和是否加底部空行 → 必要时加标题和顶部空行;正文前加两个空格并套用计划样式;可选加底部空行 → 返回最终显示行。

调用关系:emit 和 current_tail_display_lines 都调用它,是计划流样式的集中出口。

调用图:调用 3 个内部函数(proposed_plan_style, new, prefix_hyperlink_lines);被 2 处调用(current_tail_display_lines, emit);外部调用 3 个(from, with_capacity, vec!)。

tests::test_cwd737–741 ↗
fn test_cwd() -> PathBuf

作用:给测试提供一个稳定的当前目录。这样测试不用依赖具体操作系统的根目录写法。

数据流:无输入 → 读取系统临时目录 → 返回 PathBuf。

调用关系:测试里的控制器工厂都会调用它。

调用图:外部调用 1 个(temp_dir)。

tests::stream_controller743–745 ↗
fn stream_controller(width: Option<usize>) -> StreamController

作用:快速创建一个测试用普通消息控制器。它减少每个测试里的重复设置。

数据流:输入宽度 → 使用 test_cwd 和 Rich 渲染模式创建 StreamController → 返回控制器。

调用关系:大量普通消息测试都用它作为起点。

调用图:调用 1 个内部函数(new);外部调用 1 个(test_cwd)。

tests::plan_stream_controller747–749 ↗
fn plan_stream_controller(width: Option<usize>) -> PlanStreamController

作用:快速创建一个测试用计划控制器。它统一计划流测试的默认环境。

数据流:输入宽度 → 使用 test_cwd 和 Rich 模式创建 PlanStreamController → 返回控制器。

调用关系:计划相关测试都通过它创建控制器。

调用图:调用 1 个内部函数(new);外部调用 1 个(test_cwd)。

tests::lines_to_plain_strings751–762 ↗
fn lines_to_plain_strings(lines: &[ratatui::text::Line<'_>]) -> Vec<String>

作用:把带样式的终端行转成普通字符串。测试只关心文字内容时会用它。

数据流:输入 ratatui Line 列表 → 把每行所有 span 的文字拼起来 → 返回字符串列表。

调用关系:很多测试用它比较实际输出和预期输出。

调用图:外部调用 1 个(iter)。

tests::collect_streamed_lines768–787 ↗
fn collect_streamed_lines(deltas: &[&str], width: Option<usize>) -> Vec<String>

作用:模拟普通消息从多个 delta 流式输出到最终结束,并收集所有显示文字。它让测试能像真实运行一样检查结果。

数据流:输入 delta 列表和宽度 → 逐段 push、不断 tick 排队内容、最后 finalize → 去掉普通消息前缀后返回文本行。

调用关系:许多普通消息 Markdown 和表格测试用它生成实际流式输出。

调用图:外部调用 3 个(new, lines_to_plain_strings, stream_controller)。

tests::collect_plan_streamed_lines789–805 ↗
fn collect_plan_streamed_lines(deltas: &[&str], width: Option<usize>) -> Vec<String>

作用:模拟计划内容完整流式输出,并收集最终显示文字。它用于比较计划流分段输出和一次性输出是否一致。

数据流:输入 delta 列表和宽度 → 逐段 push、tick、finalize → 返回计划块显示行的普通字符串。

调用关系:计划表格和 Markdown fenced table 测试主要用它。

调用图:外部调用 3 个(new, lines_to_plain_strings, plan_stream_controller)。

tests::controller_set_width_rebuilds_queued_lines808–827 ↗
fn controller_set_width_rebuilds_queued_lines()

作用:验证窗口变窄后,已经排队但还没显示的行会按新宽度重新换行。没有这个保护,用户会看到旧宽度下的排版。

数据流:创建控制器并推入长行 → 确认先入队一行 → 改宽度到更窄 → 批量 tick 后检查输出变成多行。

调用关系:直接覆盖 StreamCore::set_width 重建队列的行为。

调用图:外部调用 4 个(assert!, assert_eq!, lines_to_plain_strings, stream_controller)。

tests::controller_set_width_no_duplicate_after_emit830–846 ↗
fn controller_set_width_no_duplicate_after_emit()

作用:验证已经显示过的内容在 resize 后不会再次入队。它防止聊天记录出现重复行。

数据流:推入并完全显示一行 → 改宽度 → 检查队列仍为空。

调用关系:针对 set_width 中“无待队列无尾巴时不要重放”的分支。

调用图:外部调用 3 个(assert!, assert_eq!, stream_controller)。

tests::controller_tick_batch_zero_is_noop849–862 ↗
fn controller_tick_batch_zero_is_noop()

作用:验证批量 tick 数量为 0 时什么也不发生。这样调用方传 0 不会误清队列。

数据流:推入一行形成队列 → 调用 batch tick 0 → 检查没有输出、未空闲、队列长度不变。

调用关系:覆盖 StreamCore::tick_batch 的保护分支。

调用图:外部调用 3 个(assert!, assert_eq!, stream_controller)。

tests::controller_has_live_tail_reflects_tail_presence865–875 ↗
fn controller_has_live_tail_reflects_tail_presence()

作用:验证普通消息的 has_live_tail 能正确反映有没有尾巴。它直接检查边界比较是否可靠。

数据流:创建控制器 → 手动放入一条渲染行并调整 enqueued_stable_len → 分别检查有尾巴和无尾巴。

调用关系:覆盖 StreamController::has_live_tail 和 StreamCore::has_tail。

调用图:外部调用 3 个(assert!, stream_controller, vec!)。

tests::plan_controller_has_live_tail_reflects_tail_presence878–888 ↗
fn plan_controller_has_live_tail_reflects_tail_presence()

作用:验证计划流的 has_live_tail 也能正确判断尾巴存在。计划和普通消息应使用同一套核心逻辑。

数据流:创建计划控制器 → 手动设置渲染行和稳定边界 → 检查 true/false 结果。

调用关系:覆盖 PlanStreamController::has_live_tail 和 StreamCore::has_tail。

调用图:外部调用 3 个(assert!, plan_stream_controller, vec!)。

tests::controller_live_tail_keeps_uncommitted_table_cell_newline_gated891–902 ↗
fn controller_live_tail_keeps_uncommitted_table_cell_newline_gated()

作用:验证表格里未换行结束的半个单元格不会出现在尾巴里。这样用户不会看到临时坏掉的表格。

数据流:推入表头、分隔行和未完成的 partial → 读取当前尾巴文字 → 确认不包含 partial。

调用关系:覆盖 push_delta 只提交完整行的规则和表格 holdback 行为。

调用图:外部调用 3 个(assert!, hyperlink_lines_to_plain_strings, stream_controller)。

tests::controller_live_tail_requires_table_holdback_state905–914 ↗
fn controller_live_tail_requires_table_holdback_state()

作用:验证普通未换行文字不会自动成为 live tail。没有表格延后状态时,半截内容应留在收集器里。

数据流:推入没有换行的普通文本 → 读取尾巴和 has_live_tail → 检查二者都为空或 false。

调用关系:针对 collector 与表格 holdback 的边界行为。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_live_tail_rerenders_table_tail_after_resize917–942 ↗
fn controller_live_tail_rerenders_table_tail_after_resize()

作用:验证表格尾巴在窗口宽度变化后会重新渲染。表格尾巴必须跟最终 Markdown 渲染保持一致。

数据流:推入表格 → 多次改变宽度 → 每次读取尾巴并和同宽度完整渲染结果比较。

调用关系:覆盖 set_width 对 live table tail 的保护和重排。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert_eq!, hyperlink_lines_to_plain_strings, lines_to_plain_strings, stream_controller)。

tests::controller_set_width_partial_drain_no_lost_lines945–968 ↗
fn controller_set_width_partial_drain_no_lost_lines()

作用:验证队列只播放了一部分时 resize 不会丢掉后面的内容。特别检查第二行仍能在 finalize 时出现。

数据流:推入长行和第二行 → tick 一次留下队列 → 改窄宽度 → finalize 并检查第二行存在。

调用关系:针对 set_width 在部分已发出、部分仍排队时的修正逻辑。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_set_width_partial_drain_keeps_pending_queue971–1003 ↗
fn controller_set_width_partial_drain_keeps_pending_queue()

作用:验证部分播放后 resize 会保留待播放队列。它确保队列还能继续 drain。

数据流:推入两行并 tick 一次 → 改宽度 → 检查仍有队列 → 持续 tick 并确认第二行出现。

调用关系:覆盖 set_width 后 rebuild_stable_queue_from_render 的实际可播放性。

调用图:外部调用 4 个(new, assert!, lines_to_plain_strings, stream_controller)。

tests::controller_set_width_preserves_in_flight_tail1006–1019 ↗
fn controller_set_width_preserves_in_flight_tail()

作用:验证没有换行的在途尾巴在 resize 后不会消失。最终结束时仍应显示这段文字。

数据流:推入未换行文本 → 改宽度 → finalize → 检查输出包含该尾巴。

调用关系:覆盖 resize 对 collector 中未完成内容的保护。

调用图:外部调用 3 个(assert_eq!, lines_to_plain_strings, stream_controller)。

tests::controller_set_width_preserves_table_tail_when_queue_is_empty1022–1050 ↗
fn controller_set_width_preserves_table_tail_when_queue_is_empty()

作用:验证队列为空但表格尾巴存在时,resize 不能误以为没有东西要保留。表头仍应留在 live tail。

数据流:先推入并播放 intro → 推入表格表头形成尾巴 → 改宽度 → 检查尾巴仍包含表头内容。

调用关系:覆盖 set_width 中 had_live_tail 的重要分支。

调用图:外部调用 4 个(assert!, assert_eq!, hyperlink_lines_to_plain_strings, stream_controller)。

tests::plan_controller_set_width_preserves_in_flight_tail1053–1072 ↗
fn plan_controller_set_width_preserves_in_flight_tail()

作用:验证计划流中未完成尾巴在 resize 后也不会丢。计划控制器应和普通消息一样安全。

数据流:推入未换行计划项 → 改宽度 → finalize → 检查输出里还有该计划项。

调用关系:覆盖 PlanStreamController 通过 StreamCore::set_width 保护尾巴的行为。

调用图:外部调用 3 个(assert!, lines_to_plain_strings, plan_stream_controller)。

tests::plan_controller_holds_table_header_as_live_tail1075–1086 ↗
fn plan_controller_holds_table_header_as_live_tail()

作用:验证计划流里的表格表头会先留作 live tail,而不是马上写进历史。这样后续分隔行到来时表格能整体重排。

数据流:推入 Intro 并排空 → 推入表格表头 → 检查没有新队列但有 live tail。

调用关系:覆盖计划流表格 holdback 对 PendingHeader 的处理。

调用图:外部调用 2 个(assert!, plan_stream_controller)。

tests::controller_loose_vs_tight_with_commit_ticks_matches_full1089–1205 ↗
fn controller_loose_vs_tight_with_commit_ticks_matches_full()

作用:验证复杂 Markdown 列表分段流式输出后,结果和一次性完整渲染一样。它防止增量提交破坏列表格式。

数据流:把松散列表和紧凑列表拆成很多 delta → 边 push 边 tick 收集输出 → 和完整 Markdown 渲染及手写预期比较。

调用关系:覆盖普通消息流式渲染与 Markdown 解析的一致性。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert_eq!, lines_to_plain_strings, stream_controller, vec!)。

tests::controller_streamed_table_matches_full_render_widths1208–1224 ↗
fn controller_streamed_table_matches_full_render_widths()

作用:验证普通表格流式输出与完整渲染一致。表格不应因为分段到达而格式不同。

数据流:分段推入表格 → 收集流式输出 → 用完整源文本渲染一次 → 比较两者。

调用关系:覆盖表格 holdback 到 finalize 后的最终一致性。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_holds_blockquoted_table_tail_until_stable1227–1243 ↗
fn controller_holds_blockquoted_table_tail_until_stable()

作用:验证引用块里的表格也会被正确延后处理。带 > 的表格不能被误当成普通文本乱提交。

数据流:流式推入 blockquote 表格 → 收集输出 → 和完整渲染比较。

调用关系:覆盖表格扫描器对引用表格的支持。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_keeps_pre_table_lines_queued_when_table_is_confirmed1246–1271 ↗
fn controller_keeps_pre_table_lines_queued_when_table_is_confirmed()

作用:验证表格确认后,表格前面的普通行仍可以独立提交。表格延后不应该把前面的文字卡住。

数据流:推入表格前说明行并确认入队 → 推入表头和分隔行 → 检查队列仍只有前文并能正常提交。

调用关系:覆盖 holdback 只从表格开始处扣住尾巴的规则。

调用图:外部调用 3 个(assert!, assert_eq!, stream_controller)。

tests::controller_set_width_during_confirmed_table_stream_matches_finalize_render1274–1307 ↗
fn controller_set_width_during_confirmed_table_stream_matches_finalize_render()

作用:验证确认中的表格遇到 resize 后,最终显示和新宽度完整渲染一致。它防止表格在宽度变化后错位。

数据流:推入表格并保持可变 → 改窄宽度 → finalize → 和同宽度完整渲染比较。

调用关系:覆盖 live table tail 的 resize 和 finalize 组合流程。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 4 个(new, assert_eq!, lines_to_plain_strings, stream_controller)。

tests::controller_does_not_hold_back_pipe_prose_without_table_delimiter1310–1323 ↗
fn controller_does_not_hold_back_pipe_prose_without_table_delimiter()

作用:验证含竖线的普通句子不会被误判成表格长期扣住。只有后面跟表格分隔线才算表格。

数据流:推入带竖线的普通行并播放 → 推入下一行 → 确认下一次能释放内容。

调用关系:覆盖表格扫描器区分 pipe prose 和 table 的能力。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_does_not_stall_repeated_pipe_prose_paragraphs1326–1345 ↗
fn controller_does_not_stall_repeated_pipe_prose_paragraphs()

作用:验证多段含竖线但不是表格的文字不会让流式输出停住。它防止错误 holdback 造成卡屏。

数据流:推入两段 pipe prose → 第二次提交时检查第一段已经能出现。

调用关系:继续覆盖非表格竖线文本的释放逻辑。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_handles_table_immediately_after_heading1348–1366 ↗
fn controller_handles_table_immediately_after_heading()

作用:验证标题后面立刻跟表格也能正确渲染。表格前没有空行或只有标题时不应破坏格式。

数据流:分段推入标题和表格 → 收集流式输出 → 和完整渲染比较。

调用关系:覆盖表格扫描和 Markdown 渲染在标题相邻场景下的一致性。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_renders_separators_for_multi_table_response_shape1369–1388 ↗
fn controller_renders_separators_for_multi_table_response_shape()

作用:验证一段回答里多个表格最终会渲染出表格分隔线。它确保真实复杂回答不会退化成原始竖线文本。

数据流:把多表格大文本按行切成 delta → 收集输出 → 检查其中出现表格分隔符。

调用关系:覆盖多表格流式处理和最终表格样式。

调用图:外部调用 2 个(assert!, collect_streamed_lines)。

tests::controller_renders_separators_for_no_outer_pipes_table_shape1391–1418 ↗
fn controller_renders_separators_for_no_outer_pipes_table_shape()

作用:验证没有外侧竖线的 Markdown 表格也能被识别和渲染。它防止这类表格表头原样残留。

数据流:流式推入含普通表格和无外侧竖线表格的文本 → 和完整渲染比较,并检查有分隔符、无原始表头。

调用关系:覆盖表格 holdback 对 no-outer-pipes 表格的支持。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 5 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings)。

tests::controller_stabilizes_first_no_outer_pipes_table_in_response1421–1449 ↗
fn controller_stabilizes_first_no_outer_pipes_table_in_response()

作用:验证回答开头附近第一个无外侧竖线表格能正确稳定下来。它防止首个特殊表格被当成普通文本。

数据流:分段推入无外侧竖线表格和后续段落 → 收集输出 → 与完整渲染比较并检查表格样式。

调用关系:覆盖 no-outer-pipes 表格作为首个表格时的路径。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 6 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_stabilizes_two_column_no_outer_table_in_response1452–1476 ↗
fn controller_stabilizes_two_column_no_outer_table_in_response()

作用:验证两列表格且没有外侧竖线时也能正确识别。两列是容易被误判成普通句子的边界情况。

数据流:推入两列无外侧竖线表格和后续段落 → 比较完整渲染,并确认原始表头不残留。

调用关系:覆盖表格扫描器对最小 no-outer-pipes 表格形态的支持。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 6 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_converts_no_outer_table_between_preboxed_sections1479–1504 ↗
fn controller_converts_no_outer_table_between_preboxed_sections()

作用:验证夹在预先画好的盒状文本之间的无外侧竖线表格仍会被转换。它防止周围复杂文本干扰表格识别。

数据流:输入含盒线和无外侧竖线表格的大段文本 → 收集流式输出 → 检查表头已转换且没有原始表头。

调用关系:覆盖复杂上下文中的表格检测和渲染。

调用图:外部调用 2 个(assert!, collect_streamed_lines)。

tests::controller_keeps_markdown_fenced_tables_mutable_until_finalize1507–1531 ↗
fn controller_keeps_markdown_fenced_tables_mutable_until_finalize()

作用:验证标记为 markdown 的代码围栏里的表格会按 Markdown 表格处理,并延后到最终确定。它不同于普通代码块。

数据流:分段推入 ```md 围栏中的表格 → 收集输出 → 和完整渲染比较,并检查有表格分隔符、无原始表头。

调用关系:覆盖表格扫描器对 markdown fence 的特殊处理。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 6 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_keeps_markdown_fenced_no_outer_tables_mutable_until_finalize1534–1562 ↗
fn controller_keeps_markdown_fenced_no_outer_tables_mutable_until_finalize()

作用:验证 markdown 围栏里的无外侧竖线表格也能被正确处理。它结合了两种特殊情况。

数据流:分段推入 ```md 中的 no-outer-pipes 表格 → 收集输出 → 和完整渲染比较并检查表格样式。

调用关系:覆盖 markdown fence 与 no-outer-pipes 表格的组合。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 6 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::controller_live_view_matches_render_during_interleaved_table_streaming1565–1608 ↗
fn controller_live_view_matches_render_during_interleaved_table_streaming()

作用:验证多段文字和多个表格交替流出时,屏幕上“已提交内容 + 当前尾巴”始终等于完整渲染快照。它检查实时画面不跑偏。

数据流:按行推入复杂源文本 → 每次排空稳定队列后,把已发内容和当前尾巴合并 → 与当前原文完整渲染比较。

调用关系:这是对稳定区、尾巴区和表格 holdback 协作的综合测试。

调用图:调用 2 个内部函数(append_markdown_agent, visible_lines);外部调用 4 个(new, assert_eq!, lines_to_plain_strings, stream_controller)。

tests::finalized_stream_table_preserves_semantic_url_fragments1611–1632 ↗
fn finalized_stream_table_preserves_semantic_url_fragments()

作用:验证表格里的长链接在最终渲染后,即使被拆成多行,链接目标仍保持完整。用户点击任何片段都应到同一个地址。

数据流:构造含长 URL 的表格 → finalize → 取显示链接行 → 检查每段 hyperlink 的 destination 都等于原 URL。

调用关系:覆盖普通消息表格渲染与终端超链接信息的配合。

调用图:外部调用 3 个(assert!, format!, stream_controller)。

tests::controller_keeps_non_markdown_fenced_tables_as_code1635–1661 ↗
fn controller_keeps_non_markdown_fenced_tables_as_code()

作用:验证非 Markdown 代码块里的竖线表格样子不会被当成表格渲染。比如 shell 代码块应原样显示。

数据流:推入 ```sh 围栏中的表格形文本 → 收集输出 → 和完整渲染比较,确认原始竖线保留且无表格分隔符。

调用关系:覆盖表格扫描器跳过非 markdown fence 的规则。

调用图:调用 1 个内部函数(append_markdown_agent);外部调用 6 个(new, assert!, assert_eq!, collect_streamed_lines, lines_to_plain_strings, vec!)。

tests::plan_controller_streamed_table_matches_final_render1664–1689 ↗
fn plan_controller_streamed_table_matches_final_render()

作用:验证计划流里的表格分段输出和一次性输出一致。计划样式不应影响表格 holdback 正确性。

数据流:分段推入计划标题和表格 → 收集输出 → 再把完整源一次性输入作基准 → 比较两者并检查表格样式。

调用关系:覆盖 PlanStreamController 与 StreamCore 表格逻辑的配合。

调用图:外部调用 4 个(assert!, assert_eq!, collect_plan_streamed_lines, vec!)。

tests::finalized_plan_stream_preserves_semantic_url_fragments1692–1717 ↗
fn finalized_plan_stream_preserves_semantic_url_fragments()

作用:验证计划表格里的长链接拆行后仍保留完整点击目标。计划块的缩进和样式不能破坏链接。

数据流:构造含长 URL 的计划表格 → finalize → 取显示链接行 → 检查所有链接目标一致。

调用关系:覆盖计划流渲染、表格和终端超链接三者组合。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert!, format!, test_cwd)。

tests::plan_controller_streamed_markdown_fenced_table_matches_final_render1720–1747 ↗
fn plan_controller_streamed_markdown_fenced_table_matches_final_render()

作用:验证计划流中 markdown 围栏里的表格,分段输出和一次性输出一致。它防止 fenced table 在计划块里退化成代码文本。

数据流:分段推入计划标题、```md 和表格 → 收集输出 → 与完整输入基准比较,并检查表格分隔符存在。

调用关系:覆盖 PlanStreamController 下 markdown fence 表格的 holdback 行为。

调用图:外部调用 4 个(assert!, assert_eq!, collect_plan_streamed_lines, vec!)。

tests::table_holdback_state_detects_header_plus_delimiter1750–1756 ↗
fn table_holdback_state_detects_header_plus_delimiter()

作用:验证表格扫描器能从表头加分隔行识别出确认表格。这是 holdback 机制的基本入口。

数据流:输入标准 pipe 表格头两行 → 调用 table_holdback_state → 检查状态为 Confirmed。

调用关系:直接测试 table_holdback 模块给本控制器提供的判断。

调用图:外部调用 1 个(assert!)。

tests::table_holdback_state_detects_single_column_header_plus_delimiter1759–1765 ↗
fn table_holdback_state_detects_single_column_header_plus_delimiter()

作用:验证单列表格也能被识别。它防止边界形态被漏掉。

数据流:输入单列表头和分隔行 → 调用扫描 → 检查状态为 Confirmed。

调用关系:覆盖表格检测的单列边界条件。

调用图:外部调用 1 个(assert!)。

tests::table_holdback_state_ignores_table_like_lines_inside_unclosed_long_fence1768–1774 ↗
fn table_holdback_state_ignores_table_like_lines_inside_unclosed_long_fence()

作用:验证未关闭的非 Markdown 长代码围栏里,即使有像表格的行也不会触发表格 holdback。

数据流:输入嵌套样式的 sh 围栏和表格形文本 → 调用扫描 → 检查状态为 None。

调用关系:覆盖扫描器对非 markdown fence 的跳过规则。

调用图:外部调用 1 个(assert!)。

tests::table_holdback_state_treats_indented_fence_text_as_plain_content1777–1786 ↗
fn table_holdback_state_treats_indented_fence_text_as_plain_content()

作用:验证缩进的围栏标记会被当成普通内容,不会开启代码围栏。后面的表格仍应被检测出来。

数据流:输入缩进 ```sh 后跟表格 → 调用扫描 → 检查状态为 Confirmed。

调用关系:覆盖 fence 识别规则里的缩进边界。

调用图:外部调用 1 个(assert!)。

tests::table_holdback_state_ignores_table_like_lines_inside_blockquoted_other_fence1789–1795 ↗
fn table_holdback_state_ignores_table_like_lines_inside_blockquoted_other_fence()

作用:验证引用块里的非 Markdown 代码围栏也会屏蔽表格检测。代码内容应原样保留。

数据流:输入 blockquote 中的 ```sh 和表格形文本 → 调用扫描 → 检查状态为 None。

调用关系:覆盖 blockquote 与非 markdown fence 组合场景。

调用图:外部调用 1 个(assert!)。

tests::incremental_holdback_matches_stateless_scan_per_chunk1798–1821 ↗
fn incremental_holdback_matches_stateless_scan_per_chunk()

作用:验证增量扫描器每收到一块后的状态,和从头扫描完整文本得到的状态一致。这样流式扫描才可信。

数据流:逐块追加文本到 source,同时推给 TableHoldbackScanner → 每步比较 scanner.state 和 table_holdback_state(source) → 必须相等。

调用关系:直接验证 StreamCore 使用的增量表格扫描器不会漂移。

调用图:调用 1 个内部函数(new);外部调用 2 个(new, assert_eq!)。

tests::incremental_holdback_detects_header_delimiter_across_chunk_boundary1824–1836 ↗
fn incremental_holdback_detects_header_delimiter_across_chunk_boundary()

作用:验证表头和分隔行分两次到达时,增量扫描器也能从疑似表头变成确认表格。

数据流:先推入表头 → 检查 PendingHeader;再推入分隔行 → 检查 Confirmed。

调用关系:覆盖流式场景最常见的跨 chunk 表格识别。

调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。

tests::controller_set_width_after_first_line_emit_does_not_requeue_first_line1839–1866 ↗
fn controller_set_width_after_first_line_emit_does_not_requeue_first_line()

作用:验证第一行已显示后再 resize,不会把第一行重新放回最终输出。它防止重复历史。

数据流:推入第一长行和第二行 → tick 出第一部分 → 改宽度 → finalize → 检查结果不含 FIRSTTOKEN 但含第二行。

调用关系:覆盖 resize 后已发内容边界的保护。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_set_width_partial_wrapped_emit_preserves_remaining_content1869–1895 ↗
fn controller_set_width_partial_wrapped_emit_preserves_remaining_content()

作用:验证一个自动换行的长句只播放了第一屏后 resize,剩余内容仍能保留。它防止同一源行的后半段丢失。

数据流:窄宽度推入长句和尾行 → tick 一次留下换行剩余 → 改宽 → finalize → 检查尾行存在。

调用关系:覆盖部分已发且同一源行跨多屏时的 resize 修复。

调用图:外部调用 2 个(assert!, stream_controller)。

tests::controller_set_width_partial_wrapped_emit_keeps_wrapped_remainder1898–1921 ↗
fn controller_set_width_partial_wrapped_emit_keeps_wrapped_remainder()

作用:验证长行在窄宽度下已显示一部分后,改宽不会丢掉未显示的后半部分单词。

数据流:推入会换成多行的长文本 → tick 一次 → 改宽到很宽 → finalize → 检查后半段关键词仍出现。

调用关系:进一步覆盖 StreamCore::set_width 对已发计数和重建队列的边界处理。

调用图:外部调用 2 个(assert!, stream_controller)。

tui/src/streaming/chunking.rs源码 ↗
domain_logicmain loop:每次终端流式输出的提交节拍要决定显示多少内容时活跃

可以把它想成汽车的两档变速箱。平时用 Smooth(平滑档),每个显示节拍只放出一行,让动画看起来稳定;如果队列里排队的行太多,或者最老的一行等太久,就切到 CatchUp(追赶档),一次把当前积压尽量放出来,避免用户看到的内容严重滞后。为了不在两个档位之间来回抖动,它用了“滞后”规则:进入追赶档的门槛高一点,退出追赶档的门槛低一点,而且低压力要持续一小段时间才退出;刚退出后还有短暂冷却,除非积压已经很严重。这个文件不关心文字从哪里来,也不重新排序,只看队列有多长、最老内容等了多久,然后给出一个 DrainPlan(排水计划,也就是这次该取一行还是取一批)。

函数细节20
AdaptiveChunkingPolicy::mode165–167 ↗
fn mode(&self) -> ChunkingMode

作用:返回当前策略处在哪个档位:平滑显示,还是快速追赶。别人用它来知道最近一次决策后的状态。

数据流:进去的是这个策略对象本身 → 它读取内部保存的 mode 字段 → 出来的是当前 ChunkingMode,不改动任何状态。

调用关系:在更外层的 resolve_chunking_plan 里会用到它,用来查看当前档位;它自己不把工作交给其他函数,只是一个状态查询口。

调用图:被 1 处调用(resolve_chunking_plan)。

AdaptiveChunkingPolicy::reset170–174 ↗
fn reset(&mut self)

作用:把策略恢复到最普通的平滑状态。适合在重新开始、清空流、或需要忘掉旧压力记录时使用。

数据流:进去的是可修改的策略对象 → 它把档位设回 Smooth,并清掉“低压力从什么时候开始”和“上次退出追赶档时间” → 出来没有返回值,但策略内部变成全新基线状态。

调用关系:它是外部代码重置这套档位记忆的入口;不像 decide 那样做判断,而是直接把所有历史痕迹清掉。

AdaptiveChunkingPolicy::decide180–210 ↗
fn decide(&mut self, snapshot: QueueSnapshot, now: Instant) -> ChunkingDecision

作用:这是这个文件的核心函数:根据现在队列里有多少行、最老一行等了多久,决定本次显示节拍该取一行还是取一批。

数据流:进去的是 QueueSnapshot(队列快照,包含排队行数和最老行年龄)以及 now(当前时间)→ 如果队列空了,就记录可能的追赶结束并回到 Smooth;如果当前是 Smooth,就尝试进入 CatchUp;如果当前是 CatchUp,就尝试退出 → 出来是 ChunkingDecision,里面写明最终档位、这次是否刚进入追赶档、以及本次 DrainPlan,同时会更新策略内部状态。

调用关系:更外层的 resolve_chunking_plan 在需要安排一次显示时调用它。它会把“要不要进入追赶档”交给 maybe_enter_catch_up,把“要不要退出追赶档”交给 maybe_exit_catch_up,队列为空时会调用 note_catch_up_exit;如果决定追赶,就生成 Batch 计划。

调用图:调用 3 个内部函数(maybe_enter_catch_up, maybe_exit_catch_up, note_catch_up_exit);被 1 处调用(resolve_chunking_plan);外部调用 1 个(Batch)。

AdaptiveChunkingPolicy::maybe_enter_catch_up216–227 ↗
fn maybe_enter_catch_up(&mut self, snapshot: QueueSnapshot, now: Instant) -> bool

作用:判断当前是否应该从平滑档切到追赶档。它只在真的发生切换的那一刻返回 true,方便外面记录一次性的状态变化。

数据流:进去的是队列快照和当前时间 → 它先看压力是否达到进入门槛,再看刚退出后的冷却期是否还有效;如果冷却期内但积压非常严重,也允许立刻进入 → 出来是 true 或 false,并可能把内部 mode 改成 CatchUp、清掉退出相关记录。

调用关系:它只由 decide 在当前处于 Smooth 时调用。它把具体判断分给 should_enter_catch_up、reentry_hold_active 和 is_severe_backlog,自己负责把这些判断合成一次档位切换。

调用图:调用 3 个内部函数(reentry_hold_active, is_severe_backlog, should_enter_catch_up);被 1 处调用(decide)。

AdaptiveChunkingPolicy::maybe_exit_catch_up233–250 ↗
fn maybe_exit_catch_up(&mut self, snapshot: QueueSnapshot, now: Instant)

作用:判断当前是否应该从追赶档退回平滑档。它不会一看到压力变小就退出,而是要求低压力持续一段时间,避免档位来回跳。

数据流:进去的是队列快照和当前时间 → 它先用 should_exit_catch_up 判断压力是否已经够低;如果不够低,就清掉低压力计时;如果够低,就记录开始变低的时间,等持续达到 EXIT_HOLD 后再切回 Smooth → 出来没有返回值,但可能更新档位和时间记录。

调用关系:它只由 decide 在当前处于 CatchUp 时调用。它使用 saturating_duration_since 安全计算持续时间,避免时间差计算出错。

调用图:调用 1 个内部函数(should_exit_catch_up);被 1 处调用(decide);外部调用 1 个(saturating_duration_since)。

AdaptiveChunkingPolicy::note_catch_up_exit252–256 ↗
fn note_catch_up_exit(&mut self, now: Instant)

作用:在队列清空等特殊情况下,记录一次追赶档结束的时间。这样后面可以启用短暂冷却,防止马上又切回追赶档。

数据流:进去的是当前时间 → 如果内部档位正是 CatchUp,就把 last_catch_up_exit_at 设成这个时间;如果本来不是 CatchUp,就什么也不做 → 出来没有返回值,只可能改一条时间记录。

调用关系:decide 在发现队列为空时会调用它。它不负责切档,只负责留下“刚退出过”的时间证据,供 reentry_hold_active 后续判断。

调用图:被 1 处调用(decide)。

AdaptiveChunkingPolicy::reentry_hold_active258–261 ↗
fn reentry_hold_active(&self, now: Instant) -> bool

作用:判断刚退出追赶档后的冷却期是否还没结束。这个冷却期用来避免刚回平滑档就马上又跳回追赶档。

数据流:进去的是当前时间,并读取内部的 last_catch_up_exit_at → 如果有上次退出时间,而且距离现在还不到 REENTER_CATCH_UP_HOLD,就返回 true;否则返回 false → 不改动状态。

调用关系:maybe_enter_catch_up 会调用它。它提供一个“现在能不能重新进追赶档”的刹车信号,但严重积压时 maybe_enter_catch_up 仍可绕过这个刹车。

调用图:被 1 处调用(maybe_enter_catch_up)。

should_enter_catch_up267–272 ↗
fn should_enter_catch_up(snapshot: QueueSnapshot) -> bool

作用:判断队列压力是否已经大到值得进入追赶档。只要排队行数够多,或者最老一行等得够久,就算需要追赶。

数据流:进去的是 QueueSnapshot → 它比较 queued_lines 和 ENTER_QUEUE_DEPTH_LINES,也比较 oldest_age 和 ENTER_OLDEST_AGE → 出来是布尔值 true 或 false,不改任何状态。

调用关系:maybe_enter_catch_up 会先问它“压力够不够大”。它是纯判断函数,不知道当前档位,也不处理冷却期。

调用图:被 1 处调用(maybe_enter_catch_up)。

should_exit_catch_up278–283 ↗
fn should_exit_catch_up(snapshot: QueueSnapshot) -> bool

作用:判断队列压力是否已经低到可以开始考虑退出追赶档。这里要求行数和等待时间两个条件都低,避免还有一个指标很糟时就过早放慢。

数据流:进去的是 QueueSnapshot → 它检查 queued_lines 是否不超过 EXIT_QUEUE_DEPTH_LINES,并且 oldest_age 是否不超过 EXIT_OLDEST_AGE → 出来是 true 或 false,不改任何状态。

调用关系:maybe_exit_catch_up 会调用它。它只回答“压力够低了吗”,至于是否已经低够久,则由 maybe_exit_catch_up 自己用时间记录判断。

调用图:被 1 处调用(maybe_exit_catch_up)。

is_severe_backlog289–294 ↗
fn is_severe_backlog(snapshot: QueueSnapshot) -> bool

作用:判断积压是否已经严重到不能再等冷却期。它是安全阀,防止刚退出追赶档后又突然堆出大量内容,导致显示延迟越滚越大。

数据流:进去的是 QueueSnapshot → 它检查 queued_lines 是否达到 SEVERE_QUEUE_DEPTH_LINES,或 oldest_age 是否达到 SEVERE_OLDEST_AGE → 出来是 true 或 false,不改任何状态。

调用关系:maybe_enter_catch_up 在冷却期内会调用它。如果它说积压很严重,maybe_enter_catch_up 就允许绕过冷却,直接回到追赶档。

调用图:被 1 处调用(maybe_enter_catch_up)。

tests::snapshot301–306 ↗
fn snapshot(queued_lines: usize, oldest_age_ms: u64) -> QueueSnapshot

作用:这是测试用的小工具,用很短的写法造出一个队列快照。测试不用每次都手写完整结构。

数据流:进去的是排队行数和最老行等待的毫秒数 → 它把毫秒数转换成 Duration(时间长度)并放进 QueueSnapshot → 出来是一个带 oldest_age 的测试快照。

调用关系:各个测试函数都会调用它来准备输入数据。它调用 from_millis 把数字毫秒变成代码里的时间类型。

调用图:外部调用 1 个(from_millis)。

tests::smooth_mode_is_default309–317 ↗
fn smooth_mode_is_default()

作用:验证默认情况下策略会保持平滑档,不会莫名其妙进入追赶档。

数据流:进去没有外部输入 → 它创建默认策略和当前时间,再构造一个压力很小的快照 → 调用 decide 后,用断言确认结果是 Smooth、没有刚进入追赶档、计划只取一行。

调用关系:这是测试入口之一。它用 default、now、snapshot 准备场景,然后通过 assert_eq! 检查 decide 的结果是否符合基线体验。

调用图:外部调用 4 个(now, assert_eq!, default, snapshot)。

tests::enters_catch_up_on_depth_threshold320–328 ↗
fn enters_catch_up_on_depth_threshold()

作用:验证只要排队行数达到进入门槛,就会切到追赶档。

数据流:进去没有外部输入 → 它创建默认策略,构造 queued_lines 为 8 的快照 → 调用 decide 后,确认模式变成 CatchUp、entered_catch_up 为 true,并计划一次取 8 行。

调用关系:这个测试覆盖 should_enter_catch_up 里的“队列长度触发”路径,但通过公开的 decide 来验证整体行为。

调用图:外部调用 4 个(now, assert_eq!, default, snapshot)。

tests::enters_catch_up_on_age_threshold331–339 ↗
fn enters_catch_up_on_age_threshold()

作用:验证即使排队行数不多,只要最老内容等得太久,也会进入追赶档。

数据流:进去没有外部输入 → 它创建默认策略,构造 queued_lines 为 2、oldest_age 为 120 毫秒的快照 → 调用 decide 后,确认进入 CatchUp,并计划取当前 2 行。

调用关系:这个测试覆盖 should_enter_catch_up 里的“等待时间触发”路径,确保延迟本身也会被重视。

调用图:外部调用 4 个(now, assert_eq!, default, snapshot)。

tests::severe_backlog_uses_faster_paced_batches342–353 ↗
fn severe_backlog_uses_faster_paced_batches()

作用:验证严重积压时,追赶档会按当前积压量整批排出,而不是仍然慢慢一行一行来。

数据流:进去没有外部输入 → 它先让策略进入 CatchUp,再给一个 64 行积压的快照 → 调用 decide 后,确认仍在 CatchUp,并且 DrainPlan 是 Batch(64)。

调用关系:这个测试通过 decide 检查批量排出行为。它用 from_millis 制造下一次节拍时间,用 snapshot 准备不同压力的队列。

调用图:外部调用 5 个(from_millis, now, assert_eq!, default, snapshot)。

tests::catch_up_batch_drains_current_backlog356–362 ↗
fn catch_up_batch_drains_current_backlog()

作用:验证追赶档的批量计划会覆盖当前全部积压行数。比如队列里有 512 行,就计划最多取 512 行。

数据流:进去没有外部输入 → 它创建一个有 512 行、等待也很久的快照 → 调用 decide → 确认模式为 CatchUp,排水计划为 Batch(512)。

调用关系:这个测试直接验证 decide 在 CatchUp 下生成 Batch 的规则,确保不会只取固定小批量而继续拖延。

调用图:外部调用 4 个(now, assert_eq!, default, snapshot)。

tests::exits_catch_up_after_hysteresis_hold365–384 ↗
fn exits_catch_up_after_hysteresis_hold()

作用:验证追赶档不会在压力刚降低时立刻退出,而是等低压力持续足够久之后才回到平滑档。

数据流:进去没有外部输入 → 它先制造高压力让策略进入 CatchUp;200 毫秒后给低压力快照,确认还没退出;460 毫秒后再次给低压力快照,确认已经回到 Smooth 并只取一行 → 输出是测试断言结果。

调用关系:这个测试主要覆盖 maybe_exit_catch_up 的滞后等待逻辑,也间接验证 should_exit_catch_up 和时间差计算配合正确。

调用图:外部调用 5 个(from_millis, now, assert_eq!, default, snapshot)。

tests::drops_back_to_smooth_when_idle387–402 ↗
fn drops_back_to_smooth_when_idle()

作用:验证队列完全空了时,策略会马上回到平滑档。没有内容可追了,就不该继续保持追赶状态。

数据流:进去没有外部输入 → 它先让策略进入 CatchUp,然后传入 queued_lines 为 0、oldest_age 为空的快照 → 调用 decide 后,确认模式变成 Smooth,计划回到 Single。

调用关系:这个测试覆盖 decide 里队列为空的特殊分支,也验证 note_catch_up_exit 会在这个场景被正确使用。

调用图:外部调用 5 个(from_millis, now, assert_eq!, default, snapshot)。

tests::holds_reentry_after_catch_up_exit405–434 ↗
fn holds_reentry_after_catch_up_exit()

作用:验证刚退出追赶档后,会有一小段时间不允许马上重新进入追赶档,避免显示策略来回抖动。

数据流:进去没有外部输入 → 它先进入 CatchUp,再用空队列让它退出;随后在冷却期内再次给达到进入门槛的快照,确认仍保持 Smooth;冷却期过后再给同样快照,确认可以重新进入 CatchUp → 输出是这些断言结果。

调用关系:这个测试覆盖 maybe_enter_catch_up 与 reentry_hold_active 的配合,说明冷却期确实会挡住普通压力下的重新进入。

调用图:外部调用 5 个(from_millis, now, assert_eq!, default, snapshot)。

tests::severe_backlog_can_reenter_during_hold437–456 ↗
fn severe_backlog_can_reenter_during_hold()

作用:验证冷却期不是死规则:如果积压已经很严重,即使刚退出追赶档,也可以立刻重新进入。

数据流:进去没有外部输入 → 它先进入 CatchUp,再用空队列退出;接着在冷却期内给 64 行严重积压快照 → 调用 decide 后,确认重新进入 CatchUp,并计划 Batch(64)。

调用关系:这个测试覆盖 maybe_enter_catch_up 调用 is_severe_backlog 后绕过 reentry_hold_active 的安全阀路径。

调用图:外部调用 5 个(from_millis, now, assert_eq!, default, snapshot)。

tui/src/streaming/commit_tick.rs源码 ↗
orchestrationmain loop / streaming commit tick

可以把这里想成一个放行员:后台不断产出文字行,先排在队列里;界面不能无脑一次全放出来,否则可能卡顿,也不能放太慢,否则用户会看到内容严重滞后。这个文件每次收到一次 commit tick(一次提交显示的时机),先查看普通输出流和计划输出流两个队列里有多少行、最老的一行等了多久,再交给 AdaptiveChunkingPolicy(自适应分块策略,也就是根据压力自动决定放多少的规则)判断:这次只放一行,还是批量放多行,或者进入追赶模式。然后它把这个决定应用到对应的 StreamController 和 PlanStreamController 上,拿到要写入历史区的 HistoryCell(历史显示单元)。它不直接改界面,也不安排下一次 tick,只负责“看队列、定方案、执行一次放行、把结果交回去”。这样显示顺序能保持正确,因为它只从队列头部取内容,就像排队买票只能从最前面开始叫号。

函数细节8
CommitTickOutput::default53–59 ↗
fn default() -> Self

作用:创建一个“这次没有提交任何内容”的默认结果。有人会在 tick 被跳过,或者还没真正处理控制器时,用它作为安全的空结果。

数据流:进去不需要任何输入 → 它新建一个空的 cells 列表,把 has_controller 设为 false,把 all_idle 设为 true → 出来一个 CommitTickOutput,表示没有产生历史显示单元,也没有发现控制器需要处理。

调用关系:run_commit_tick 在发现当前不该真正放行内容时会用它直接返回;apply_commit_tick_plan 也先用它打底,然后再根据实际控制器逐步填入结果。它内部会创建新的空列表。

调用图:被 2 处调用(apply_commit_tick_plan, run_commit_tick);外部调用 1 个(new)。

run_commit_tick69–91 ↗
fn run_commit_tick(
    policy: &mut AdaptiveChunkingPolicy,
    stream_controller: Option<&mut StreamController>,
    plan_stream_controller: Option<&mut PlanStreamController>,
    scope: CommitTickS

作用:这是本文件的主入口:执行一次“提交滴答”。它决定这次要不要从流式队列里取内容、取多少,并返回这次产生的历史显示单元。

数据流:进去的是分块策略 policy、两个可选的流控制器、这次 tick 的范围 scope,以及当前时间 now → 它先调用 stream_queue_snapshot 看队列压力,再调用 resolve_chunking_plan 让策略决定放行方案;如果 scope 要求只能在追赶模式放行,但当前不是追赶模式,就返回默认空结果;否则调用 apply_commit_tick_plan 真正从控制器队列头部取内容 → 出来 CommitTickOutput,里面有本次产生的 cells,以及控制器是否存在、是否都空闲。

调用关系:外层的流式显示循环会在每次 commit tick 时调用它。它自己不做底层取行,而是把“看队列”交给 stream_queue_snapshot,把“问策略”交给 resolve_chunking_plan,把“执行放行”交给 apply_commit_tick_plan。

调用图:调用 4 个内部函数(default, apply_commit_tick_plan, resolve_chunking_plan, stream_queue_snapshot)。

stream_queue_snapshot97–118 ↗
fn stream_queue_snapshot(
    stream_controller: Option<&StreamController>,
    plan_stream_controller: Option<&PlanStreamController>,
    now: Instant,
) -> QueueSnapshot

作用:把当前所有流控制器的排队情况合成一张快照。策略需要这张快照来判断现在是轻松显示,还是已经积压到必须加速追赶。

数据流:进去的是普通流控制器、计划流控制器,以及当前时间 now → 它分别读取每个控制器排了多少行,并计算最老排队内容已经等了多久;两个控制器都有数据时,把行数相加,把等待时间取更大的那个 → 出来 QueueSnapshot,包含总排队行数和最久等待时间。

调用关系:run_commit_tick 会先调用它,因为后面的策略判断不能凭空决定。它在比较两个等待时间时,把小工具 max_duration 叫来帮忙取较大的 Duration(时间长度)。

调用图:调用 1 个内部函数(max_duration);被 1 处调用(run_commit_tick)。

resolve_chunking_plan124–142 ↗
fn resolve_chunking_plan(
    policy: &mut AdaptiveChunkingPolicy,
    snapshot: QueueSnapshot,
    now: Instant,
) -> ChunkingDecision

作用:让自适应分块策略根据当前队列压力做一次决定,并在模式变化时写一条跟踪日志。这样排查“为什么突然批量显示”时有线索。

数据流:进去的是可修改的 policy、队列快照 snapshot、当前时间 now → 它先读取策略原来的模式,再调用 policy.decide 得到新的 ChunkingDecision(分块决定,里面包含模式和放行方案);如果模式变了,就用 trace 日志记录前后模式、排队行数、最老等待时间等信息 → 出来这次策略决定。

调用关系:run_commit_tick 在拿到队列快照后调用它。它是策略和执行之间的中间站:自己不取内容,只负责拿到 DrainPlan(放行计划)并提供统一的模式变化日志。

调用图:调用 2 个内部函数(decide, mode);被 1 处调用(run_commit_tick);外部调用 1 个(trace!)。

apply_commit_tick_plan148–173 ↗
fn apply_commit_tick_plan(
    drain_plan: DrainPlan,
    stream_controller: Option<&mut StreamController>,
    plan_stream_controller: Option<&mut PlanStreamController>,
) -> CommitTickOutput

作用:按照已经决定好的放行计划,实际去两个流控制器里取内容。它把每个控制器吐出的显示单元收集起来,合成一次 tick 的输出。

数据流:进去的是 DrainPlan,以及两个可选的可修改控制器 → 它先创建默认空输出;如果普通流控制器存在,就标记 has_controller,并调用 drain_stream_controller 取内容;如果计划流控制器存在,也同样调用 drain_plan_stream_controller;每拿到一个 cell 就放进输出列表,同时把每个控制器是否空闲合并成 all_idle → 出来 CommitTickOutput,告诉外层本次生成了哪些历史单元、是否还有控制器、是否都已经没活了。

调用关系:run_commit_tick 在确认可以执行放行后调用它。它不直接关心策略为什么这么决定,只负责把 DrainPlan 分发给普通流控制器和计划流控制器,并把两个结果拼成一个统一结果。

调用图:调用 3 个内部函数(default, drain_plan_stream_controller, drain_stream_controller);被 1 处调用(run_commit_tick)。

drain_stream_controller180–188 ↗
fn drain_stream_controller(
    controller: &mut StreamController,
    drain_plan: DrainPlan,
) -> (Option<Box<dyn HistoryCell>>, bool)

作用:对普通流控制器执行一次放行动作。放行计划说只取一行,它就取一行;计划说批量取,它就按上限批量取。

数据流:进去的是一个可修改的 StreamController 和 DrainPlan → 如果计划是 Single,就调用 controller.on_commit_tick;如果计划是 Batch(max_lines),就调用 controller.on_commit_tick_batch(max_lines) → 出来一个可选的 HistoryCell,以及这个控制器是否已经空闲。

调用关系:apply_commit_tick_plan 在处理普通输出流时调用它。它是统一策略和具体控制器接口之间的适配层,让上层不用关心普通控制器具体该调单行方法还是批量方法。

调用图:调用 2 个内部函数(on_commit_tick, on_commit_tick_batch);被 1 处调用(apply_commit_tick_plan)。

drain_plan_stream_controller194–202 ↗
fn drain_plan_stream_controller(
    controller: &mut PlanStreamController,
    drain_plan: DrainPlan,
) -> (Option<Box<dyn HistoryCell>>, bool)

作用:对计划流控制器执行一次放行动作。它和普通流控制器的处理方式保持一致,保证两类流都听同一套分块策略。

数据流:进去的是一个可修改的 PlanStreamController 和 DrainPlan → 如果计划是 Single,就调用 controller.on_commit_tick;如果计划是 Batch(max_lines),就调用 controller.on_commit_tick_batch(max_lines) → 出来一个可选的 HistoryCell,以及这个计划流控制器是否已经空闲。

调用关系:apply_commit_tick_plan 在处理计划输出流时调用它。它的作用和 drain_stream_controller 对称,只是面向另一种控制器类型,避免两类流的放行规则走偏。

调用图:调用 2 个内部函数(on_commit_tick, on_commit_tick_batch);被 1 处调用(apply_commit_tick_plan)。

max_duration207–214 ↗
fn max_duration(lhs: Option<Duration>, rhs: Option<Duration>) -> Option<Duration>

作用:在两个可能存在、也可能不存在的时间长度里,选出更大的那个。这里用它来找“所有队列里最老的一条内容等了多久”。

数据流:进去的是 lhs 和 rhs 两个 Option<Duration>;Option 的意思是“可能有值,也可能没有值” → 如果两个都有值,就返回较大的;如果只有一个有值,就返回那个;如果两个都没有,就返回 None → 出来一个同样可能为空的时间长度。

调用关系:stream_queue_snapshot 在合并两个控制器的最老等待时间时调用它。它是一个小工具函数,让快照计算保持简单,并正确处理某个控制器没有排队内容的情况。

调用图:被 1 处调用(stream_queue_snapshot)。

转录单元基础

这些文件提供核心转录单元抽象,以及流式处理和事件投影所发出的具体历史单元渲染器。

tui/src/history_cell/mod.rs源码 ↗
domain_logicmain loop / rendering / transcript overlay

聊天窗口不是直接把一大段文字糊到屏幕上,而是拆成一个个 HistoryCell,也就是“历史单元”。它可以是一条用户消息、一段模型回复、一次命令输出、一个计划更新等。这个文件就是这些单元的总入口:一边把 approvals、exec、messages、plans 等子模块重新导出,方便别处使用;一边定义 HistoryCell 这个共同约定。每个单元都要能回答:“我在普通聊天窗口显示哪些行?”“复制友好的纯文本是什么?”“在 Ctrl+T 全文记录里怎么显示?”“给定终端宽度后我需要占几行?”文件还处理富文本和纯文本两种模式,富文本里可能带终端超链接。最后,它让 Box<dyn HistoryCell> 这种“装着任意历史单元的盒子”可以被真正画到终端缓冲区里,并在重绘前清空旧内容,避免流式更新或窗口缩放后留下残影。

函数细节16
raw_lines_from_source150–164 ↗
fn raw_lines_from_source(source: &str) -> Vec<Line<'static>>

作用:把一整段原始文本按换行拆成多行,方便后面显示成终端里的历史内容。它会处理末尾多出来的换行,避免凭空多显示一条空行。

数据流:输入是一段字符串 → 如果字符串为空就直接得到空列表;否则按换行符切开,并在原文以换行结尾时去掉最后那个空片段 → 输出是一组没有样式的 Line,每个 Line 对应屏幕上的一条逻辑行。

调用关系:它会被 render_source 使用,通常是在某个历史单元需要把原始源码或原始文本拿出来显示时,先把大块文本切成适合界面消费的行。

调用图:被 1 处调用(render_source);外部调用 1 个(new)。

plain_lines166–178 ↗
fn plain_lines(lines: impl IntoIterator<Item = Line<'static>>) -> Vec<Line<'static>>

作用:把带颜色、样式或其他显示信息的行,压平成普通文字行。这样复制、原始模式显示或缓存时,不会夹杂界面装饰。

数据流:输入是一批 Line,每行里可能有多个带样式的小片段 → 它逐个取出这些片段的文字内容并拼起来,丢掉样式 → 输出是一批纯文本 Line。

调用关系:多个 raw_lines 实现会调用它。它像一个“去妆”步骤:富文本单元先生成漂亮显示,再通过它变成干净的纯文本版本。

调用图:被 4 处调用(raw_lines, raw_lines, raw_lines, raw_lines);外部调用 1 个(into_iter)。

HistoryCell::display_lines_for_mode201–206 ↗
fn display_lines_for_mode(&self, width: u16, mode: HistoryRenderMode) -> Vec<Line<'static>>

作用:根据当前显示模式,决定返回漂亮版还是纯文本版的可见行。这样同一个历史单元能同时服务普通聊天窗口和原始滚动记录。

数据流:输入是终端宽度和 HistoryRenderMode 模式 → 如果是 Rich,就取带链接的显示内容再抽出可见文字;如果是 Raw,就直接取 raw_lines → 输出是一批真正要显示的 Line。

调用关系:desired_height_for_mode 会调用它来量高度。它也间接依赖 display_hyperlink_lines 和 visible_lines,把“带链接的数据”转换成“屏幕能看到的文字”。

调用图:调用 2 个内部函数(display_hyperlink_lines, visible_lines);被 1 处调用(desired_height_for_mode)。

HistoryCell::desired_height226–228 ↗
fn desired_height(&self, width: u16) -> u16

作用:计算这个历史单元在普通聊天窗口里需要占多少行高度。终端很窄时,一条长文字会自动折成多行,所以不能只数原始行数。

数据流:输入是终端宽度 → 它默认按 Rich 模式交给 desired_height_for_mode 计算 → 输出是这个单元需要的屏幕行数。

调用关系:渲染布局阶段会用它来决定每个历史单元占多高。Box::desired_height 也会转交到这里,让装在盒子里的不同单元都能统一量尺寸。

调用图:调用 1 个内部函数(desired_height_for_mode);被 3 处调用(desired_height, desired_height, desired_height)。

HistoryCell::desired_height_for_mode230–236 ↗
fn desired_height_for_mode(&self, width: u16, mode: HistoryRenderMode) -> u16

作用:按指定模式精确估算历史单元显示后会占几行。它用终端 UI 库的换行算法来算,避免长网址、长命令等把高度算错。

数据流:输入是终端宽度和显示模式 → 先拿到该模式下的显示行,再交给 Paragraph 这个终端段落组件,按不裁剪空白的方式测量自动换行后的行数 → 输出是 u16 形式的高度,算不出来时给 0。

调用关系:desired_height 会调用它。它处在“准备布局”的位置,保证后面的绘制区域大小和实际文字换行结果一致。

调用图:调用 1 个内部函数(display_lines_for_mode);被 1 处调用(desired_height);外部调用 2 个(new, from)。

HistoryCell::transcript_lines243–245 ↗
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成 Ctrl+T 全文记录窗口里要显示的行。默认和普通聊天窗口一样,但某些单元可以改写成更适合查阅或复制的样子。

数据流:输入是终端宽度 → 默认直接调用 display_lines → 输出是一批用于全文记录窗口的 Line。

调用关系:transcript_hyperlink_lines 和 render_transcript 会用它。比如命令执行单元可以覆盖它,在全文记录里显示更完整的命令和退出状态。

调用图:被 2 处调用(transcript_hyperlink_lines, render_transcript)。

HistoryCell::desired_transcript_height261–279 ↗
fn desired_transcript_height(&self, width: u16) -> u16

作用:计算这个历史单元在 Ctrl+T 全文记录窗口里需要占几行。它还专门绕开了一个终端 UI 库的小问题:只有空白的一行不该被算成两行。

数据流:输入是终端宽度 → 先拿 transcript_hyperlink_lines,再取出可见文字;如果只有一行且全是空白,就直接返回 1;否则用 Paragraph 按实际换行测量 → 输出是全文记录窗口中的高度。

调用关系:全文记录覆盖层布局时会用它。它和普通 desired_height 类似,但针对 transcript 内容,因为全文记录的展示可能和主聊天窗口不同。

调用图:调用 2 个内部函数(transcript_hyperlink_lines, visible_lines);外部调用 2 个(new, from)。

HistoryCell::is_stream_continuation281–283 ↗
fn is_stream_continuation(&self) -> bool

作用:告诉外部:这个历史单元是不是一次流式输出的延续。默认不是。

数据流:没有额外输入,只读取单元自身的实现 → 默认直接返回 false → 不修改任何状态。

调用关系:display_lines_for_history_insert 会查看它,用来决定把新内容插入历史时是否应该当作上一段流式内容的延续。需要这种行为的具体单元会覆盖它。

调用图:被 1 处调用(display_lines_for_history_insert)。

HistoryCell::transcript_animation_tick295–297 ↗
fn transcript_animation_tick(&self) -> Option<u64>

作用:告诉全文记录窗口:这个单元的显示是否会随时间变化,比如加载动画或闪烁提示。默认返回 None,表示内容稳定。

数据流:没有额外输入,只由单元自身判断 → 默认返回 None → 不产生状态变化;如果某个单元覆盖它,可能返回一个会随时间变的数字。

调用关系:它服务于 Ctrl+T 全文记录的缓存刷新。因为全文记录会缓存正在生成的尾部内容,如果动画单元不提供变化信号,全文记录里可能看起来卡在第一帧。

Box::render301–318 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把一个装在 Box 里的历史单元真正画到终端屏幕缓冲区上。Box 可以理解成“盒子”,里面装的是任意一种 HistoryCell。

数据流:输入是绘制区域 area 和终端缓冲区 buf → 它先按区域宽度拿到带链接的显示行,再取出可见文字,算出如果内容太高应该从哪里开始滚动;然后清空整块区域、画段落、最后把超链接标记写回缓冲区 → 输出是被更新后的终端缓冲区。

调用关系:终端主绘制流程会调用它,因为 Box<dyn HistoryCell> 实现了 Renderable。它把工作分给 visible_lines、Paragraph 绘制和 mark_buffer_hyperlinks,完成从“历史数据”到“屏幕像素格”的最后一步。

调用图:调用 2 个内部函数(mark_buffer_hyperlinks, visible_lines);外部调用 4 个(new, from, try_from, from)。

Box::desired_height319–321 ↗
fn desired_height(&self, width: u16) -> u16

作用:让装在 Box 里的历史单元也能被统一询问高度。调用方不用知道盒子里具体是哪种消息。

数据流:输入是终端宽度 → 它把请求转交给盒子内部那个 HistoryCell 的 desired_height → 输出是内部单元需要的显示高度。

调用关系:布局系统面对的是 Box<dyn HistoryCell>,所以需要这个转接函数。它本身不计算,只是把问题递给真正的历史单元。

调用图:调用 1 个内部函数(desired_height)。

HistoryCell::as_any325–327 ↗
fn as_any(&self) -> &dyn Any

作用:把 HistoryCell 暂时当成 Any 来看,方便外部在确实需要时判断它具体是哪一种单元。Any 可以理解成 Rust 里的“运行时类型识别入口”。

数据流:输入是一个不可修改的 HistoryCell 引用 → 它返回同一个对象的 Any 视角 → 不改变对象内容。

调用关系:当上层逻辑拿着通用 HistoryCell,但又需要识别某个具体实现时,会用到它。它提供的是安全的只读类型检查通道。

HistoryCell::as_any_mut329–331 ↗
fn as_any_mut(&mut self) -> &mut dyn Any

作用:把 HistoryCell 暂时当成可修改的 Any 来看,方便外部在确认具体类型后修改它。这个能力主要给正在流式变化的活动单元使用。

数据流:输入是一个可修改的 HistoryCell 引用 → 它返回同一个对象的可修改 Any 视角 → 本函数自己不改内容,但调用者随后可以在确认类型后修改对象。

调用关系:当聊天界面需要更新某个正在生成的历史单元,而手上只有通用 HistoryCell 时,会通过它进入具体类型。它是“通用容器”和“具体单元更新”之间的桥。

tui/src/history_cell/messages.rs源码 ↗
domain_logicmain loop / rendering

这个文件像聊天窗口的“排版工”。它不负责产生回答,而是负责把已有内容排成用户能看的终端文本。用户消息会带上引用片段、远程图片占位,并按窗口宽度换行;助手消息会保留项目符号、缩进和终端超链接;推理摘要会用较淡的斜体显示,也能只放进 transcript(文字记录)而不显示在界面上;流式输出时,尚未稳定的最后几行会单独显示,避免正在生成的表格被二次换行弄坏。一个重要点是:助手最终消息会保存原始 Markdown(带格式的文本源),窗口大小变化时重新渲染,而不是拿旧的屏幕行硬拉伸,所以表格和链接能保持正确。

函数细节30
build_user_message_lines_with_elements18–78 ↗
fn build_user_message_lines_with_elements(
    message: &str,
    elements: &[TextElement],
    style: Style,
    element_style: Style,
) -> Vec<Line<'static>>

作用:把用户输入的一整段文字拆成多行,并把其中被标记的片段用另一种颜色显示。这样用户引用文件、选择片段之类的内容,在历史记录里能一眼看出来。

数据流:输入是一段消息、若干带字节范围的 TextElement(标记片段)、普通样式和高亮样式。它先按位置排序,再逐行扫描消息,把普通文字和高亮片段交错拼成 Span;如果某个范围切在中文等 UTF-8 字符中间,就跳过,避免界面崩掉。输出是一组可直接渲染的 Line。

调用关系:它是 UserHistoryCell::display_lines 的内部帮手。用户消息里有 text_elements 时,display_lines 会先让它把原文变成带样式的逻辑行,再交给换行器按终端宽度排版。

调用图:被 1 处调用(display_lines);外部调用 6 个(from, from, styled, new, sort_by_key, to_vec)。

remote_image_display_line80–82 ↗
fn remote_image_display_line(style: Style, index: usize) -> Line<'static>

作用:生成一行远程图片的占位文字,比如“第几张图片”。终端里不能直接显示远程图片时,就用这行提示用户这里有图片。

数据流:输入是显示样式和图片序号。它把序号交给图片标签生成函数,再套上样式,输出一行 Line。

调用关系:它服务于用户消息的图片展示流程,通常在构造 UserHistoryCell 的显示内容时使用,让远程图片 URL 不直接刷屏,而是变成简洁的图片标签。

调用图:外部调用 1 个(from)。

trim_trailing_blank_lines84–92 ↗
fn trim_trailing_blank_lines(mut lines: Vec<Line<'static>>) -> Vec<Line<'static>>

作用:删掉一组显示行末尾多余的空白行。这样用户输入末尾有很多回车时,历史记录不会留下难看的大片空白。

数据流:输入是一组 Line。它从最后一行开始检查,只要这一行所有文字去掉空格后都是空的,就弹掉;遇到有内容的行就停止。输出是清理后的行列表。

调用关系:UserHistoryCell::display_lines 在换行完成后会调用它,专门收尾,保证真正显示前已经去掉末尾空行。

调用图:被 1 处调用(display_lines)。

UserHistoryCell::display_lines95–177 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把一条用户消息排成终端里看到的样子,包括开头符号、换行、彩色标记和远程图片占位。它是用户发言显示出来的主入口。

数据流:输入是终端宽度,以及这个 cell 里保存的消息文本、文本标记和远程图片列表。它先算出可用宽度,再分别处理图片和文字:图片变成标签行,普通文字直接按行换行,带标记文字则先高亮片段再换行;最后加上用户消息前缀和上下空行。输出是可显示的 Line 列表。

调用关系:界面刷新聊天历史时会调用它。它会把细活交给 build_user_message_lines_with_elements、trim_trailing_blank_lines,以及通用的换行和加前缀工具。

调用图:调用 3 个内部函数(build_user_message_lines_with_elements, trim_trailing_blank_lines, new);外部调用 4 个(from, new, from, vec!)。

UserHistoryCell::raw_lines179–193 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:拿到用户消息的“原始文本版”,不带界面前缀和颜色。这个版本适合复制、保存 transcript,或者做不需要终端样式的处理。

数据流:输入是 cell 内的消息和远程图片列表。它先把消息按原文拆行并去掉末尾换行;如果有远程图片,就在文字后面空一行,再追加图片标签。输出是朴素的 Line 列表。

调用关系:它是 HistoryCell 接口的一部分,供 transcript、测试或其他需要原始内容的地方使用;它不走 display_lines 那套漂亮排版。

调用图:外部调用 1 个(from)。

ReasoningSummaryCell::new208–215 ↗
fn new(header: String, content: String, cwd: &Path, transcript_only: bool) -> Self

作用:创建一块“推理摘要”历史记录。它会记住当时的工作目录,这样摘要里的本地文件链接以后仍按当时的环境解释。

数据流:输入是标题、正文、当前工作目录 cwd,以及是否只进 transcript 的标记。它把这些值存进 ReasoningSummaryCell,并把 cwd 复制一份。输出是一个新的摘要 cell。

调用关系:new_reasoning_summary_block 会用它生成摘要;测试和从线程记录转成 transcript 的流程也会用它,确保摘要渲染时环境一致。

调用图:被 5 处调用(new_reasoning_summary_block, reasoning_summary_height_matches_wrapped_rendering_for_url_like_content, source_backed_cells_render_raw_source_without_prefix_or_style, wrapped_and_prefixed_cells_handle_tiny_widths, thread_to_transcript_cells);外部调用 1 个(to_path_buf)。

ReasoningSummaryCell::lines217–244 ↗
fn lines(&self, width: u16) -> Vec<Line<'static>>

作用:把推理摘要正文渲染成带样式、带项目符号、会自动换行的显示行。它是摘要在界面和 transcript 中排版的共同核心。

数据流:输入是终端宽度和 cell 内保存的 Markdown 正文、cwd。它先用 Markdown 渲染器把正文变成行,并用 cwd 处理本地文件链接;再给所有文字加上淡色斜体;最后加上“• ”这样的缩进并按宽度换行。输出是排好版的 Line 列表。

调用关系:ReasoningSummaryCell::display_lines 和 ReasoningSummaryCell::transcript_lines 都调用它,所以摘要在屏幕和 transcript 里的换行规则保持一致。

调用图:调用 2 个内部函数(usable_content_width_u16, new);被 2 处调用(display_lines, transcript_lines);外部调用 3 个(as_path, default, new)。

ReasoningSummaryCell::display_lines248–254 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:决定推理摘要要不要显示在聊天界面上。如果这段摘要被标记为只进 transcript,它在界面上就不占位置。

数据流:输入是终端宽度和 transcript_only 标记。若 transcript_only 为真,直接输出空列表;否则调用 lines 生成可显示的摘要行。它不修改 cell 本身。

调用关系:界面渲染历史记录时会调用它。真正的排版工作交给 ReasoningSummaryCell::lines。

调用图:调用 1 个内部函数(lines);外部调用 1 个(new)。

ReasoningSummaryCell::transcript_lines256–258 ↗
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成推理摘要写入 transcript 时使用的行。即使界面上可能隐藏,它也可以在文字记录里出现。

数据流:输入是宽度。它直接调用 lines,用同一套 Markdown 和换行规则生成输出行。它不改动内部数据。

调用关系:transcript 导出或记录流程会调用它;它复用 ReasoningSummaryCell::lines,避免屏幕版和记录版排版逻辑分叉。

调用图:调用 1 个内部函数(lines)。

ReasoningSummaryCell::raw_lines260–266 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:返回推理摘要的原始正文行,不带淡色斜体、不带项目符号。如果摘要只给 transcript,它在这里反而返回空,避免被普通历史显示重复拿到。

数据流:输入是 cell 内的 content 和 transcript_only。若 transcript_only 为真,输出空列表;否则把正文 trim 后拆成原始行并输出。

调用关系:这是 HistoryCell 的原始内容接口,供需要纯文本来源的地方使用,和 display_lines 的漂亮渲染路线分开。

调用图:外部调用 1 个(new)。

AgentMessageCell::new277–282 ↗
fn new(lines: Vec<Line<'static>>, is_first_line: bool) -> Self

作用:从普通终端行创建一块助手消息 cell。它主要给测试和一些简单场景用,把不带链接的行包装成助手消息。

数据流:输入是一组 Line 和一个 is_first_line 标记。它把普通行转成没有超链接信息的 HyperlinkLine,再保存是否是助手回复的第一行。输出是新的 AgentMessageCell。

调用关系:测试里常用它构造样本;真正带链接的运行路径更多会用 AgentMessageCell::new_hyperlink_lines。

调用图:被 4 处调用(consolidation_walker_replaces_agent_message_cells, empty_agent_message_cell_transcript, streamed_agent_list_paragraph_preserves_item_indent_when_wrapped, wrapped_and_prefixed_cells_handle_tiny_widths)。

AgentMessageCell::display_lines293–295 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把助手消息转成普通可见行,用于终端显示。它隐藏了超链接的内部信息,只留下用户看得见的文字。

数据流:输入是终端宽度。它先调用 display_hyperlink_lines 得到带链接和换行后的行,再抽出可见文本。输出是 Vec<Line>。

调用关系:聊天界面只需要显示文字时会调用它;具体排版交给 AgentMessageCell::display_hyperlink_lines。

调用图:调用 1 个内部函数(display_hyperlink_lines)。

AgentMessageCell::raw_lines323–325 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:取出助手消息的纯文本原始行,不带终端超链接包装。适合做复制、比较或保存原文。

数据流:输入是 cell 内保存的 HyperlinkLine。它先取出可见行,再转成普通 Line。输出是不带链接元数据的行列表。

调用关系:这是 HistoryCell 的原始内容接口,供不需要界面样式的流程使用;它不参与显示换行。

AgentMessageCell::is_stream_continuation327–329 ↗
fn is_stream_continuation(&self) -> bool

作用:告诉外部:这块助手消息是不是流式输出的续段,而不是一条回复的开头。这样界面知道要不要显示开头的项目符号。

数据流:输入是内部的 is_first_line 标记。它返回这个标记的反值:不是第一行就表示是续段。不会修改任何数据。

调用关系:流式消息合并、历史渲染或判断连续块时会问它,用来区分助手回复开头和后续片段。

AgentMarkdownCell::new355–360 ↗
fn new(markdown_source: String, cwd: &Path) -> Self

作用:创建一块已经完成的助手 Markdown 消息,并保存原始 Markdown 源文本。这样窗口大小变化时可以重新渲染,而不是沿用旧的折行结果。

数据流:输入是原始 markdown_source 和当时的 cwd。它把源文本存下,并复制 cwd。输出是新的 AgentMarkdownCell。

调用关系:流式回复结束后,handle_consolidate_agent_message 会用它把一串临时 AgentMessageCell 合并成一个源文本驱动的 cell;很多测试也验证它在不同宽度下能重新排版。

调用图:被 10 处调用(handle_consolidate_agent_message, agent_markdown_cell_does_not_split_words_after_inline_markdown, agent_markdown_cell_narrow_width_shows_prefix_only, agent_markdown_cell_renders_source_at_different_widths, agent_markdown_cell_survives_insert_history_rewrap, consolidation_walker_replaces_agent_message_cells, source_backed_cells_render_raw_source_without_prefix_or_style, wrapped_and_prefixed_cells_handle_tiny_widths, transcript_overlay_live_tail_preserves_semantic_web_links, thread_to_transcript_cells);外部调用 1 个(to_path_buf)。

AgentMarkdownCell::display_lines364–366 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把最终助手 Markdown 消息变成屏幕可见的普通行。它保留排版结果,但不暴露超链接内部结构。

数据流:输入是终端宽度。它调用 display_hyperlink_lines 重新渲染 Markdown,再取出可见文字行。输出是 Vec<Line>。

调用关系:界面渲染最终助手回复时调用它;真正的 Markdown 渲染和链接处理都在 AgentMarkdownCell::display_hyperlink_lines 里完成。

调用图:调用 1 个内部函数(display_hyperlink_lines)。

AgentMarkdownCell::raw_lines393–395 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:返回助手最终消息的原始 Markdown 行。它不加项目符号,也不做终端样式处理。

数据流:输入是保存的 markdown_source。它按源文本拆成 raw lines 并输出,不改动内部状态。

调用关系:供复制、保存原始记录或测试使用;它和 display_hyperlink_lines 的渲染路线不同,后者负责漂亮显示。

StreamingAgentTailCell::new410–415 ↗
fn new(lines: Vec<HyperlinkLine>, is_first_line: bool) -> Self

作用:创建正在流式输出时的“尾巴”cell。尾巴指还没稳定写进历史区的最后几行,尤其是可能还没生成完的表格。

数据流:输入是已经渲染好的 HyperlinkLine 列表,以及这些行是否从助手消息第一行开始。它把二者保存起来,输出新的 StreamingAgentTailCell。

调用关系:sync_active_stream_tail 会在流式内容变化时创建它;测试也会用它检查空白行等临时显示行为。

调用图:被 2 处调用(sync_active_stream_tail, streaming_agent_tail_blank_line_uses_one_viewport_row)。

StreamingAgentTailCell::display_lines419–421 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把正在流式输出的尾巴转成屏幕可见行。它是临时内容显示时的普通文本入口。

数据流:输入是宽度参数。它调用 display_hyperlink_lines 得到带前缀的尾巴行,再抽出可见文字。输出是 Vec<Line>。

调用关系:界面显示 active_cell 时会用它;raw_lines 也会借它生成一个纯文本版本。

调用图:调用 1 个内部函数(display_hyperlink_lines);被 1 处调用(raw_lines)。

StreamingAgentTailCell::raw_lines453–455 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:返回流式尾巴的纯文本行,通常用于调试、复制或接口要求的原始行。它用一个极大宽度避免人为窄屏影响。

数据流:输入是 cell 内保存的尾巴行。它调用 display_lines,并传入 u16::MAX 作为宽度,再转成普通 Line。输出是不带超链接元数据的行列表。

调用关系:这是 HistoryCell 的 raw_lines 实现;它依赖 StreamingAgentTailCell::display_lines,而 display_lines 又会走 display_hyperlink_lines。

调用图:调用 1 个内部函数(display_lines)。

StreamingAgentTailCell::is_stream_continuation457–459 ↗
fn is_stream_continuation(&self) -> bool

作用:告诉外部这段流式尾巴是不是助手回复的续段。这个信息决定前缀和合并行为。

数据流:输入是内部 is_first_line。它返回反值:不是第一行就说明是续段。没有副作用。

调用关系:流式渲染和历史整理逻辑会用它判断这段内容是否接在前面助手消息后面。

new_user_prompt461–473 ↗
fn new_user_prompt(
    message: String,
    text_elements: Vec<TextElement>,
    local_image_paths: Vec<PathBuf>,
    remote_image_urls: Vec<String>,
) -> UserHistoryCell

作用:创建一条用户输入历史记录。调用方把文字、标记片段、本地图片路径和远程图片 URL 交给它,它打包成 UserHistoryCell。

数据流:输入是 message、text_elements、local_image_paths 和 remote_image_urls。它不加工内容,只把这些字段放进结构体。输出是一个 UserHistoryCell。

调用关系:用户提交 prompt 后,上层代码会用它生成历史 cell;之后显示时由 UserHistoryCell::display_lines 和 raw_lines 决定怎么看、怎么导出。

new_reasoning_summary_block478–510 ↗
fn new_reasoning_summary_block(
    full_reasoning_buffer: String,
    cwd: &Path,
) -> Box<dyn HistoryCell>

作用:把一整段推理缓冲区整理成一个推理摘要历史 cell。它会判断这段内容有没有真正的摘要,避免把空壳摘要塞进界面。

数据流:输入是完整推理文本和当前 cwd。它先 trim,再寻找 Markdown 粗体标记“...”作为头部;如果头部后面还有内容,就把头部和摘要正文分开,创建一个可显示的 ReasoningSummaryCell;如果没有可显示摘要,就创建 transcript_only 的 cell。输出是装箱的 HistoryCell。

调用关系:推理块结束时会调用它生成历史记录。它把实际 cell 构造交给 ReasoningSummaryCell::new,并复制 cwd,保证以后渲染本地文件链接时不受后续切换目录影响。

调用图:调用 1 个内部函数(new);外部调用 2 个(new, to_path_buf)。

tui/src/history_cell/plans.rs源码 ↗
domain_logicmain loop / streaming / history rendering / resize reflow

在这个终端界面里,AI 不只是输出普通文字,还会给出计划。这个文件就像一个“排版工”,把计划内容变成用户容易读的样子:标题、缩进、空行、颜色、复选框样式都会在这里处理。它区分两类情况:一种是已经完成的计划,保存原始 Markdown(带格式的文本),以后终端变宽变窄时还能重新排版;另一种是正在流式出现的计划,只保存已经画好的几行,适合临时预览。文件里还处理“计划更新”,比如用 表示完成,用 □ 表示待办或进行中。重要的是,它不只是为了好看,还保证历史记录、复制出来的原文、带链接的显示内容,都能各取所需。

函数细节20
StreamingPlanTailCell::new17–22 ↗
fn new(lines: Vec<HyperlinkLine>, is_stream_continuation: bool) -> Self

作用:创建一个“正在流式生成的计划尾巴”单元。它用来临时显示还没最终写进历史记录的那一小段计划内容。

数据流:进去的是已经排好版的多行内容,以及一个标记,说明它是不是上一段流式内容的延续;函数把这两样东西装进 StreamingPlanTailCell;出来的是一个可被界面拿去临时显示的对象。

调用关系:当同步当前活跃流式尾巴的流程需要一块临时计划预览时,sync_active_stream_tail 会调用它。之后终端界面会通过 HistoryCell 这套统一接口来读取它的显示行和原始行。

调用图:被 1 处调用(sync_active_stream_tail)。

StreamingPlanTailCell::display_lines26–28 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>

作用:把流式计划尾巴转换成终端真正可见的普通显示行。这里不重新排版,因为内容进来时已经排好了。

数据流:进去的是单元里保存的带链接行;函数去掉隐藏链接信息,只保留用户能看见的文字和样式;出来的是终端可以直接画出来的 Line 列表。

调用关系:它是 HistoryCell 接口的一部分,界面刷新时会用它拿到可见内容。它依赖已有的 lines,不再去碰原始 Markdown。

StreamingPlanTailCell::raw_lines38–40 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:拿到流式计划尾巴的纯文本版本,适合复制、日志或不需要颜色链接的地方。

数据流:进去的是保存的带链接显示行;函数先取可见文字,再去掉样式和链接;出来的是普通 Line 列表。

调用关系:这是 HistoryCell 统一要求提供的能力。和显示函数不同,它面向“原文式内容”,让外部不用关心这个单元内部是否带链接。

StreamingPlanTailCell::is_stream_continuation42–44 ↗
fn is_stream_continuation(&self) -> bool

作用:告诉外部:这块流式计划尾巴是不是前一块内容的继续。这样界面可以决定是否把它当成同一段流来展示。

数据流:进去的是对象内部保存的布尔标记;函数原样返回;不会生成新内容,也不会修改状态。

调用关系:界面或历史拼接逻辑会通过 HistoryCell 接口问这个问题,用来避免把连续流式输出误显示成多个独立块。

new_plan_update47–50 ↗
fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell

作用:把一次计划更新的数据包装成 PlanUpdateCell。这个单元后面会被画成类似待办清单的样子。

数据流:进去的是 UpdatePlanArgs,里面有可选说明文字和计划步骤;函数把这两部分拆出来放进 PlanUpdateCell;出来的是一个能参与历史渲染的计划更新对象。

调用关系:当系统收到“计划已更新”的信息时会用它造历史单元。真正的屏幕排版随后由 PlanUpdateCell::display_lines 完成。

new_proposed_plan57–62 ↗
fn new_proposed_plan(plan_markdown: String, cwd: &Path) -> ProposedPlanCell

作用:创建一个最终版的“拟议计划”历史单元。它保存原始 Markdown,所以以后终端宽度变化时还能重新排版。

数据流:进去的是计划的 Markdown 文本和当前工作目录 cwd;函数把 Markdown 存下,并把 cwd 复制成自己的路径;出来的是 ProposedPlanCell。

调用关系:流式计划结束、需要把临时内容固化进历史时,会使用这个函数。它之后会通过 ProposedPlanCell::display_hyperlink_lines 根据当前宽度重新渲染。

调用图:外部调用 1 个(to_path_buf)。

new_proposed_plan_stream68–76 ↗
fn new_proposed_plan_stream(
    lines: Vec<impl Into<HyperlinkLine>>,
    is_stream_continuation: bool,
) -> ProposedPlanStreamCell

作用:创建一个临时的“正在输出中的拟议计划”单元。它适合边生成边显示,但不适合长期保存为最终历史。

数据流:进去的是已经渲染好的若干行,以及是否为流式延续的标记;函数把每一行转成 HyperlinkLine 并保存;出来的是 ProposedPlanStreamCell。

调用关系:流式输出过程中会用它快速塞入历史显示。等计划完整后,应该换成保存原始 Markdown 的 ProposedPlanCell,这样以后才能按新宽度重排。

ProposedPlanCell::display_lines101–103 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把最终版拟议计划变成终端上可见的普通行。它会先生成带链接版本,再取出用户能看到的部分。

数据流:进去的是终端宽度;函数调用 display_hyperlink_lines 按这个宽度渲染;出来的是去掉链接隐藏信息后的可见 Line 列表。

调用关系:界面刷新最终计划卡片时会走到这里。它把主要工作交给 ProposedPlanCell::display_hyperlink_lines,自己只负责转换成普通显示行。

调用图:调用 1 个内部函数(display_hyperlink_lines)。

ProposedPlanCell::raw_lines133–135 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:返回最终计划的纯文本原始内容。它适合复制、保存日志,或者不需要终端样式的地方。

数据流:进去的是对象保存的 Markdown;函数把它拆成原始文本行;出来的是不带颜色、不带链接的 Line 列表。

调用关系:这是 HistoryCell 接口要求的一部分。和 display_hyperlink_lines 不同,它不做漂亮排版,只保留计划本身的文字。

ProposedPlanStreamCell::display_lines139–141 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>

作用:把正在流式输出的拟议计划转换成终端可见行。它不重新渲染,因为这些行本来就是实时生成好的。

数据流:进去的是单元保存的带链接行;函数提取可见部分;出来的是普通显示行列表。

调用关系:流式输出时界面刷新会用它。它和最终版 ProposedPlanCell 的区别是:它没有原始 Markdown,不能根据新宽度重新排版。

ProposedPlanStreamCell::raw_lines151–153 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成流式拟议计划的纯文本版本。适合不需要颜色、链接和终端样式的地方。

数据流:进去的是保存的带链接显示行;函数取出可见文字,再转成朴素行;出来的是纯文本 Line 列表。

调用关系:这是 HistoryCell 的统一出口之一。它让外部可以把流式计划当普通历史内容读取,而不用知道里面存的是 HyperlinkLine。

ProposedPlanStreamCell::is_stream_continuation155–157 ↗
fn is_stream_continuation(&self) -> bool

作用:说明这个流式拟议计划片段是不是前一个片段的延续。这个信息能帮助界面把连续输出接在一起看。

数据流:进去的是对象保存的 continuation 标记;函数原样返回;没有副作用。

调用关系:历史显示或流式合并逻辑会通过 HistoryCell 接口使用它,判断该不该把这个片段当作同一段流的一部分。

PlanUpdateCell::display_lines167–217 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把计划更新画成用户友好的待办清单。它会显示标题、可选说明,以及每一步的状态图标和样式。

数据流:进去的是终端宽度、可选说明 explanation、步骤列表 plan;函数先准备标题,再把说明文字按宽度换行,把步骤按状态变成 或 □,并加上缩进;出来的是终端可直接显示的带样式行。

调用关系:当历史里出现 PlanUpdateCell 时,界面刷新会调用它。它内部使用换行和缩进工具,把较长说明或步骤折成适合当前窗口宽度的多行。

调用图:外部调用 2 个(from, vec!)。

PlanUpdateCell::raw_lines219–237 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成计划更新的纯文本版本。它保留标题、说明和每一步状态,但去掉颜色、复选框样式和终端排版。

数据流:进去的是 explanation 和 plan;函数先写入“Updated Plan”,再加入非空说明;如果没有步骤就写“(no steps provided)”,否则逐条写成“状态: 步骤”;出来的是普通 Line 列表。

调用关系:这是 PlanUpdateCell 给复制、日志、转录等场景提供的简洁版本。它不参与漂亮显示,那部分由 PlanUpdateCell::display_lines 完成。

调用图:外部调用 3 个(from, format!, vec!)。

tui/src/history_cell/approvals.rs源码 ↗
domain_logicrequest handling

在这个终端界面里,Codex 做一些敏感动作前可能要经过审核,比如运行命令、联网、改文件。这个文件就像“审核结果播报员”:它拿到审核对象和审核结果后,拼出一行好读的提示,并配上绿色对勾或红色叉。为了避免命令太长刷屏,它会先把命令整理成短摘要:去掉多余包装,只保留第一行,最长截到 80 个可见字符左右。核心函数 new_approval_decision_cell 会根据不同情况写出不同句子,比如“你批准本次运行某命令”“自动审核器拒绝了请求”“网络访问规则已保存”。另外还有几组专门给自动审核器用的函数,用来显示补丁申请或普通动作申请被批准、拒绝、超时。最后的 new_review_status_line 用青色文字显示“正在审核”这类状态。

函数细节11
truncate_exec_snippet5–12 ↗
fn truncate_exec_snippet(full_cmd: &str) -> String

作用:把一整条命令压缩成适合显示在历史记录里的一小段。这样即使原命令很长、很多行,也不会把终端界面撑乱。

数据流:进去的是完整命令字符串 → 它先只取第一行,如果原命令有换行,就在第一行后面加上省略提示 → 再把文字截短到大约 80 个可见字符 → 出来的是一个短命令摘要,不改动外部数据。

调用关系:它是命令摘要的最后一道“裁剪工序”。exec_snippet 会先把命令整理成字符串,再交给它负责压短。

调用图:被 1 处调用(exec_snippet);外部调用 1 个(format!)。

exec_snippet14–17 ↗
fn exec_snippet(command: &[String]) -> String

作用:把程序内部保存的命令数组,变成一段适合给人看的短命令文字。有人要在界面上说明“刚才批准/拒绝的是哪条命令”时会用它。

数据流:进去的是一组命令片段,比如程序名和参数 → 它先把 shell 包装之类不适合直接展示的部分去掉并转义整理 → 再调用 truncate_exec_snippet 截成短摘要 → 出来的是能放进历史记录的一小段命令文字。

调用关系:new_approval_decision_cell 在需要展示命令内容时会直接用它;non_empty_exec_snippet 也会借它生成摘要,然后再判断摘要是不是空。

调用图:调用 1 个内部函数(truncate_exec_snippet);被 2 处调用(new_approval_decision_cell, non_empty_exec_snippet)。

non_empty_exec_snippet19–22 ↗
fn non_empty_exec_snippet(command: &[String]) -> Option<String>

作用:生成命令摘要,并且只在摘要真的有内容时返回它。这样界面不会显示一段空命令,避免出现别扭的提示语。

数据流:进去的是命令数组 → 它调用 exec_snippet 得到短摘要 → 如果摘要为空,就返回“没有内容”;如果不为空,就返回这段文字 → 它不修改任何外部状态。

调用关系:new_approval_decision_cell 用它来决定一句话里要不要带上具体命令。命令为空时,界面会改说“这个请求”,而不是硬塞一个空白命令。

调用图:调用 1 个内部函数(exec_snippet);被 1 处调用(new_approval_decision_cell)。

new_approval_decision_cell45–264 ↗
fn new_approval_decision_cell(
    subject: ApprovalDecisionSubject,
    decision: ReviewDecision,
    actor: ApprovalDecisionActor,
) -> Box<dyn HistoryCell>

作用:根据审核对象、审核结果、做决定的人,生成一条完整的历史记录。它是这个文件最核心的“造句器”。

数据流:进去的是三样东西:审核对象,比如命令或网络目标;审核决定,比如批准、拒绝、超时、取消;以及决定者,比如用户或自动审核器 → 它按情况挑选绿色对勾或红色叉,再拼出人能读懂的句子,必要时调用 non_empty_exec_snippet 或 exec_snippet 插入命令摘要 → 出来的是一个 HistoryCell,也就是终端历史区能显示的一块内容。

调用关系:当审核流程结束后,界面层会调用它把结果放进聊天/历史记录。它把命令摘要工作交给 exec_snippet 和 non_empty_exec_snippet,把最终显示交给 PrefixedWrappedHistoryCell 这种带前缀、会自动换行的历史单元。

调用图:调用 3 个内部函数(exec_snippet, non_empty_exec_snippet, new);外部调用 4 个(new, from, from, vec!)。

ApprovalDecisionActor::subject273–278 ↗
fn subject(self) -> &'static str

作用:把“谁做了决定”翻译成一句提示开头。比如用户就是“You ”,自动审核器就是“Auto-reviewer ”。

数据流:进去的是 ApprovalDecisionActor 这个枚举值,也就是 User 或 Guardian → 它做一个简单匹配 → 出来的是固定英文短语,用来拼进历史记录句子里。

调用关系:new_approval_decision_cell 在拼“谁批准了”“谁拒绝了”这类句子时会用它。它让主函数不用到处手写这些称呼。

new_guardian_denied_patch_request281–301 ↗
fn new_guardian_denied_patch_request(files: Vec<String>) -> Box<dyn HistoryCell>

作用:生成一条“自动审核器拒绝应用补丁”的历史记录。补丁就是一组准备改文件的变更。

数据流:进去的是补丁会碰到的文件列表 → 它判断是一个文件还是多个文件:一个文件就显示文件名,多个文件就显示数量 → 再配上红色叉和“denied”文字 → 出来的是可显示的 HistoryCell。

调用关系:当自动审核器拒绝 Codex 修改文件时,界面会用它记录原因概况。它不再细分每个修改内容,只告诉用户这次补丁影响了哪些文件或多少文件。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, from, from, vec!)。

new_guardian_denied_action_request303–311 ↗
fn new_guardian_denied_action_request(summary: String) -> Box<dyn HistoryCell>

作用:生成一条“自动审核器拒绝某个普通动作”的历史记录。这里的动作由调用方用一句 summary 概括。

数据流:进去的是动作摘要字符串 → 它把摘要放到“Request denied for ...”这句话里,并把摘要做成较弱的显示样式 → 配上红色叉 → 出来的是 HistoryCell。

调用关系:当被拒绝的不是专门的命令、网络或补丁格式,而是一个可用文字概括的动作时,会用这个函数。它把具体动作说明留给调用方提供。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, from, vec!)。

new_guardian_approved_action_request313–321 ↗
fn new_guardian_approved_action_request(summary: String) -> Box<dyn HistoryCell>

作用:生成一条“自动审核器批准某个普通动作”的历史记录。它和拒绝版本类似,只是结果是批准。

数据流:进去的是动作摘要字符串 → 它拼成“Request approved for ...” → 给整条记录配绿色对勾 → 出来的是 HistoryCell,不改动外部数据。

调用关系:当自动审核器通过了某个用 summary 描述的动作时,界面会调用它。它和 new_guardian_denied_action_request 是一对,用同样格式表达相反结果。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, from, vec!)。

new_guardian_timed_out_patch_request323–343 ↗
fn new_guardian_timed_out_patch_request(files: Vec<String>) -> Box<dyn HistoryCell>

作用:生成一条“补丁审核超时”的历史记录。也就是说,还没等到批准,Codex 就不能继续应用这次文件修改。

数据流:进去的是补丁影响的文件列表 → 它判断是单个文件还是多个文件,分别显示文件名或文件数量 → 拼成“Review timed out before codex could apply ...” → 配红色叉 → 出来的是 HistoryCell。

调用关系:当自动审核器或审核流程等太久没有结果时,这个函数负责把补丁请求的超时情况写进历史记录。它和 new_guardian_denied_patch_request 格式相近,但表达的是“没等到结果”,不是明确拒绝。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, from, from, vec!)。

new_guardian_timed_out_action_request345–353 ↗
fn new_guardian_timed_out_action_request(summary: String) -> Box<dyn HistoryCell>

作用:生成一条“普通动作审核超时”的历史记录。它告诉用户某个动作因为审核没及时完成而没有继续。

数据流:进去的是动作摘要字符串 → 它拼成“Review timed out before ...”这句话 → 把摘要作为淡色文字放进去,并配红色叉 → 出来的是 HistoryCell。

调用关系:当一个非补丁类动作等待审核超时,界面会用它显示结果。它和 new_guardian_denied_action_request 一样依赖调用方提供动作摘要。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, from, vec!)。

new_review_status_line356–360 ↗
fn new_review_status_line(message: String) -> PlainHistoryCell

作用:生成一行青色的当前审核状态文字,比如提示用户正在等待审核。它不是最终结果,而是过程中的状态提示。

数据流:进去的是一段状态消息 → 它把消息染成青色,并包进 PlainHistoryCell 这种简单历史单元里 → 出来的是一条可显示的状态行。

调用关系:在审核还没结束时,界面可以调用它显示进度。等审核结束后,通常会再用 new_approval_decision_cell 或其他结果函数显示最终批准、拒绝或超时。

调用图:外部调用 1 个(vec!)。

tui/src/history_cell/exec.rs源码 ↗
domain_logicmain loop / history rendering

后台终端的事情如果不记录,用户很容易迷糊:程序到底只是等了一下,还是给某个后台命令发了输入?现在后台还有几个命令没结束?这个文件把这些信息做成“历史记录格子”(一条能显示在界面历史里的记录)。UnifiedExecInteractionCell 显示一次交互:有输入就写“Interacted”,没输入就写“Waited”,还会附上命令名,并按窗口宽度自动换行。UnifiedExecProcessesCell 显示 /ps 的结果:列出最多 16 个后台终端,命令太长会安全截短,还会展示最近输出片段。它特别注意 Unicode 宽度,也就是中文、 emoji 这类字符占几格的问题,避免界面排版歪掉。

函数细节9
UnifiedExecInteractionCell::new12–17 ↗
fn new(command_display: Option<String>, stdin: String) -> Self

作用:创建一条“后台终端交互”的历史记录。调用者把要显示的命令名和实际输入的内容交给它,它只负责把这两份数据收好。

数据流:进去的是一个可选的命令显示文本和一段 stdin 输入文本;它把这两项放进 UnifiedExecInteractionCell 里;出来的是一个以后可以被界面渲染或导出为纯文本的记录对象,不会额外改动别的东西。

调用关系:这是最底层的建造函数。正常流程里,new_unified_exec_interaction 会调用它;测试也会直接调用它,检查 URL 这类文本不会被错误拆开,以及显示高度和实际换行是否一致。

调用图:被 3 处调用(new_unified_exec_interaction, unified_exec_interaction_cell_does_not_split_url_like_stdin_token, unified_exec_interaction_cell_height_matches_wrapped_rendering)。

UnifiedExecInteractionCell::display_lines21–63 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把一次后台终端交互变成适合屏幕显示的多行文字。它会根据当前窗口宽度换行,并用缩进和样式让用户一眼看出这是等待还是输入。

数据流:进去的是界面宽度,以及对象里保存的命令名和 stdin;如果宽度是 0,就直接给空结果;否则先做一行标题,再在有 stdin 时把输入内容按宽度换行并缩进;出来的是一组带样式的 Line,供终端界面直接画出来。

调用关系:它是 HistoryCell 这类历史记录格子的屏幕渲染入口。渲染历史时外部会调用它;它内部借助 Line、vec! 以及自适应换行工具,把原始字符串整理成界面能显示的行。

调用图:调用 1 个内部函数(new);外部调用 3 个(from, new, vec!)。

UnifiedExecInteractionCell::raw_lines65–95 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成不带复杂排版的纯文本版本,适合复制、日志或无样式输出。它保留关键事实:是等待后台终端,还是与后台终端交互,以及相关命令和输入内容。

数据流:进去的是对象里保存的命令名和 stdin;如果 stdin 为空,就输出“Waited for background terminal”;如果有输入,就输出“Interacted with background terminal”,再追加输入原文拆出的行;出来的是一组普通 Line,不会做屏幕宽度换行。

调用关系:它和 display_lines 是同一条记录的两种出口:display_lines 给屏幕看,raw_lines 给纯文本场景用。它会用格式化字符串和 raw_lines_from_source 这类辅助工具,把输入文本拆成更原始的行。

调用图:外部调用 3 个(from, new, format!)。

new_unified_exec_interaction98–103 ↗
fn new_unified_exec_interaction(
    command_display: Option<String>,
    stdin: String,
) -> UnifiedExecInteractionCell

作用:这是创建后台终端交互记录的便捷入口。外部代码不用直接关心结构体怎么初始化,只要把命令名和输入交进来即可。

数据流:进去的是可选命令显示文本和 stdin;它把这些原样交给 UnifiedExecInteractionCell::new;出来的是一个 UnifiedExecInteractionCell,可以放进历史记录里显示。

调用关系:它是外部流程和 UnifiedExecInteractionCell::new 之间的小门面。后台终端发生等待或输入后,上层代码会通过它生成历史记录。

调用图:调用 1 个内部函数(new)。

UnifiedExecProcessesCell::new111–113 ↗
fn new(processes: Vec<UnifiedExecProcessDetails>) -> Self

作用:创建一条“后台终端进程汇总”的记录。它把一组后台进程详情收起来,等界面需要时再渲染成列表。

数据流:进去的是多个 UnifiedExecProcessDetails,每个里面有命令显示文本和最近输出片段;它把这组数据保存到 UnifiedExecProcessesCell;出来的是一个可渲染的进程汇总对象。

调用关系:它只被 new_unified_exec_processes_output 调用。也就是说,外部一般不会直接拿这个内部格子,而是通过更完整的 /ps 复合历史记录来使用它。

调用图:被 1 处调用(new_unified_exec_processes_output)。

UnifiedExecProcessesCell::display_lines123–225 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把当前后台终端列表画成适合屏幕显示的多行内容。它负责标题、空列表提示、命令截短、最近输出片段,以及“还有更多进程”的提示。

数据流:进去的是界面宽度和保存的进程列表;宽度为 0 时返回空;否则先输出标题,列表为空就显示没有后台终端;列表不空就最多显示 16 个进程,把命令和输出片段按可用宽度截短,必要时加上“[...]”;出来的是带颜色和样式的 Line 列表。

调用关系:这是 /ps 汇总内容真正被画出来的地方。UnifiedExecProcessesCell::raw_lines 会借它生成纯文本,UnifiedExecProcessesCell::desired_height 也会借它计算需要多少行;它内部使用字符宽度计算和截断工具,保证中文、宽字符和长命令不会把界面撑坏。

调用图:被 2 处调用(desired_height, raw_lines);外部调用 5 个(from, width, new, format!, vec!)。

UnifiedExecProcessesCell::raw_lines227–229 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成后台终端列表的纯文本版本。它让同一份 /ps 信息不只可以显示在彩色界面里,也能用于复制或普通文本输出。

数据流:进去的是进程汇总对象本身;它先用 display_lines 以极大宽度生成完整显示行,再用 plain_lines 去掉样式;出来的是更朴素的 Line 列表。

调用关系:它依赖 UnifiedExecProcessesCell::display_lines,说明纯文本输出和屏幕输出走的是同一套内容规则,只是最后把颜色、粗体这类样式拿掉。

调用图:调用 1 个内部函数(display_lines)。

UnifiedExecProcessesCell::desired_height231–233 ↗
fn desired_height(&self, width: u16) -> u16

作用:告诉界面这条后台进程列表大概需要占多少行。界面布局时可以用它决定留多高的空间。

数据流:进去的是当前窗口宽度;它调用 display_lines 按这个宽度实际算出会显示哪些行;出来的是这些行的数量,转换成高度值。

调用关系:它是布局阶段用的小接口。它不自己重新计算规则,而是复用 UnifiedExecProcessesCell::display_lines,所以显示内容变了,高度也会自然跟着一致。

调用图:调用 1 个内部函数(display_lines)。

new_unified_exec_processes_output236–242 ↗
fn new_unified_exec_processes_output(
    processes: Vec<UnifiedExecProcessDetails>,
) -> CompositeHistoryCell

作用:创建完整的 /ps 历史输出。它不只包含后台进程列表,还会先放一行紫色的“/ps”,让用户知道这是哪条命令产生的结果。

数据流:进去的是后台进程详情列表;它先创建一个显示“/ps”的普通历史格子,再创建进程汇总格子,最后把二者合成一个 CompositeHistoryCell(组合历史格子,也就是把多块内容拼成一条记录);出来的是可直接放进历史记录的完整输出。

调用关系:这是外部生成后台进程汇总记录的主要入口。它会调用 PlainHistoryCell::new、UnifiedExecProcessesCell::new 和 CompositeHistoryCell::new,把命令标记和实际结果装配到一起。

调用图:调用 3 个内部函数(new, new, new);外部调用 1 个(vec!)。

tui/src/history_cell/mcp.rs源码 ↗
domain_logicmain loop / request handling / transcript rendering

MCP 可以理解成“外部工具插座”:程序通过它去调用别的服务提供的工具。这个文件做的是终端界面里的展示层,把这些工具调用变成一条条聊天历史记录。它会先显示“正在调用哪个服务器的哪个工具”,调用结束后改成成功或失败,并把文字结果折行、截断后放在下面,避免一大坨输出把屏幕冲垮。如果结果里藏着图片,它不会直接把图片铺开,而是额外加一条“有图片输出”的提示。它还负责 /mcp 命令的显示:没有服务器时给出文档链接,有服务器时列出认证状态、工具、资源和模板。另有一个临时加载单元,用小动画告诉用户“工具清单还在取”。整体像前台接待员:后台工具真正干活,这里负责把过程和结果用人能看懂的方式摆出来。

函数细节24
CompletedMcpToolCallWithImageOutput::display_lines10–12 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>

作用:把“工具返回了图片”这件事显示成终端里的一行提示。它不展示图片本身,只告诉用户这次结果里有图片输出。

数据流:进去的是这个图片结果单元和屏幕宽度;它不使用宽度,也不读取图片内容,只生成一行文字 tool result (image output);出来的是给终端显示用的行列表,不改动任何状态。

调用关系:它是图片结果历史单元的显示接口。图片单元由工具调用完成时额外创建,之后聊天历史渲染时会调用它来显示这条提示。

调用图:外部调用 1 个(vec!)。

CompletedMcpToolCallWithImageOutput::raw_lines14–16 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:给“原始文本版本”的聊天记录也补上一行图片结果提示。这样复制日志或保存转录时,也能知道这里曾经有图片输出。

数据流:进去的是这个图片结果单元;它直接生成一行纯文字提示;出来的是不带复杂样式的行列表,不改动图片或其他数据。

调用关系:它和 CompletedMcpToolCallWithImageOutput::display_lines 是一对:一个给屏幕看,一个给原始记录看。

调用图:外部调用 1 个(vec!)。

mcp_auth_status_label18–25 ↗
fn mcp_auth_status_label(status: McpAuthStatus) -> &'static str

作用:把 MCP 认证状态变成人能读懂的英文标签,比如“未登录”或“OAuth”。OAuth 是一种常见的网页授权登录方式。

数据流:进去的是一个认证状态枚举;它按状态做简单对应;出来的是固定文字,不读取也不修改外部数据。

调用关系:生成 /mcp 工具清单时会用它,把服务器的认证状态显示在界面上。

McpToolCallCell::new44–57 ↗
fn new(
        call_id: String,
        invocation: McpInvocation,
        animations_enabled: bool,
    ) -> Self

作用:新建一条“正在调用 MCP 工具”的历史记录。它会记下调用编号、调用了谁、从什么时候开始,以及是否允许动画。

数据流:进去的是调用 ID、服务器/工具/参数信息、动画开关;它记录当前时间作为开始时间,把结果和耗时先留空;出来的是一个还没完成的工具调用显示单元。

调用关系new_active_mcp_tool_call 会调用它,通常是在后台刚开始执行某个 MCP 工具时创建这条活动记录。

调用图:被 1 处调用(new_active_mcp_tool_call);外部调用 1 个(now)。

McpToolCallCell::call_id59–61 ↗
fn call_id(&self) -> &str

作用:取出这次工具调用的编号。调用编号像快递单号,用来把后续返回的结果对应回正确的那一条记录。

数据流:进去的是工具调用单元;它只借出内部保存的 call_id 字符串引用;出来的是这个编号,不修改任何内容。

调用关系:外层界面或状态管理代码需要查找、匹配某次调用时会用它。它本身不继续调用别的本文件函数。

McpToolCallCell::complete63–73 ↗
fn complete(
        &mut self,
        duration: Duration,
        result: Result<codex_protocol::mcp::CallToolResult, String>,
    ) -> Option<Box<dyn HistoryCell>>

作用:把一条“正在调用”的记录改成“已完成”,并保存耗时和结果。如果结果里有能解码的图片,还会顺手造一条额外的图片提示记录。

数据流:进去的是耗时和工具返回结果,结果可能成功也可能是错误文字;它先尝试从结果中找图片,再把耗时和结果写进自己;出来的是可选的额外历史单元,只有检测到图片时才有。

调用关系:后台工具调用结束时会用它。它把“检查图片结果”的活交给 try_new_completed_mcp_tool_call_with_image_output,自己负责更新主工具调用单元。

调用图:调用 1 个内部函数(try_new_completed_mcp_tool_call_with_image_output)。

McpToolCallCell::success75–81 ↗
fn success(&self) -> Option<bool>

作用:判断这次工具调用现在算成功、失败,还是还没结束。它把内部结果统一变成一个简单的三态答案。

数据流:进去的是工具调用单元当前保存的结果;如果还没有结果就返回空,如果返回错误就算失败,如果成功结果里标了 is_error 也算失败;出来的是可选的真假值,不改状态。

调用关系McpToolCallCell::display_lines 用它决定显示绿色点、红色点还是加载动画;McpToolCallCell::raw_lines 用它决定标题写“Calling”还是“Called”。

调用图:被 2 处调用(display_lines, raw_lines)。

McpToolCallCell::mark_failed83–87 ↗
fn mark_failed(&mut self)

作用:在调用被中断时,把这条记录标成失败。比如程序停止、连接断开,不能一直让它显示“正在调用”。

数据流:进去的是可修改的工具调用单元;它用开始时间算出已经过了多久,把耗时写进去,并把结果写成错误 interrupted;出来没有单独返回值,但这个单元状态从进行中变成失败。

调用关系:外层流程发现调用没法正常完成时会用它。之后显示函数会看到失败状态,并显示红色失败提示。

调用图:外部调用 1 个(elapsed)。

McpToolCallCell::render_content_block89–116 ↗
fn render_content_block(block: &serde_json::Value, width: usize) -> String

作用:把工具返回的一个内容块变成适合终端看的文字。内容可能是文字、图片、音频、资源或链接,它会分别用简短方式表示。

数据流:进去的是一段 JSON 内容和可用宽度;它先尝试按 MCP 内容格式解析,文字会被格式化并截断,图片显示成 <image content>,音频显示成 <audio content>,资源和链接显示地址;出来是一段字符串。

调用关系McpToolCallCell::display_linesMcpToolCallCell::raw_lines 都靠它把复杂的工具输出压成可读文本。它也会调用通用的格式化/截断工具,防止输出太长。

调用图:外部调用 3 个(clone, to_string, format!)。

McpToolCallCell::display_lines120–213 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成这条 MCP 工具调用在终端屏幕上该怎么显示。它负责标题、颜色、动画、折行、树状缩进和结果摘要。

数据流:进去的是工具调用单元和屏幕宽度;它读取成功状态、调用信息、结果内容和动画开关,决定用绿色点、红色点还是活动指示器;然后把调用名和结果内容按宽度折行;出来的是带样式的显示行,不修改状态。

调用关系:聊天历史每次重绘时会调用它。它会用 McpToolCallCell::success 判断状态,用 format_mcp_invocation 生成“服务器.工具(参数)”标题,用 McpToolCallCell::render_content_block 渲染结果内容。

调用图:调用 4 个内部函数(success, format_mcp_invocation, from_animations_enabled, new);外部调用 6 个(from, render_content_block, new, format!, clone, vec!)。

McpToolCallCell::raw_lines215–239 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成这条 MCP 工具调用的纯文本版本。它适合复制、保存日志,或不需要彩色样式的地方使用。

数据流:进去的是工具调用单元;它读取状态和结果,把标题写成 CallingCalled,再把每个结果内容块转成文字;出来的是纯文本行列表,不改动状态。

调用关系:它和 display_lines 服务同一条历史记录,只是面向原始文本。它也会调用 McpToolCallCell::successMcpToolCallCell::render_content_block,保证屏幕显示和文本记录含义一致。

调用图:调用 1 个内部函数(success);外部调用 4 个(from, render_content_block, format!, vec!)。

McpToolCallCell::transcript_animation_tick241–246 ↗
fn transcript_animation_tick(&self) -> Option<u64>

作用:告诉界面这条“正在调用”的记录需不需要继续刷新动画。它用时间切成小步,让加载符号能动起来。

数据流:进去的是工具调用单元;如果动画关闭或调用已经有结果,就返回空;否则根据开始到现在的毫秒数除以 50 算出一个动画刻度;出来的是可选数字,不改状态。

调用关系:终端转录/聊天历史的动画刷新机制会读取它。只要工具还没完成,它就能推动 display_lines 里的活动指示器更新。

调用图:外部调用 1 个(elapsed)。

new_active_mcp_tool_call249–255 ↗
fn new_active_mcp_tool_call(
    call_id: String,
    invocation: McpInvocation,
    animations_enabled: bool,
) -> McpToolCallCell

作用:提供一个更直白的入口,用来创建“正在执行的 MCP 工具调用”单元。外部代码不必直接碰结构体构造细节。

数据流:进去的是调用 ID、调用内容和动画开关;它把这些原样交给 McpToolCallCell::new;出来的是新的 McpToolCallCell

调用关系:这是外层聊天界面创建活动工具调用记录时会用的便捷函数,内部实际工作交给 McpToolCallCell::new

调用图:调用 1 个内部函数(new)。

try_new_completed_mcp_tool_call_with_image_output268–279 ↗
fn try_new_completed_mcp_tool_call_with_image_output(
    result: &Result<codex_protocol::mcp::CallToolResult, String>,
) -> Option<CompletedMcpToolCallWithImageOutput>

作用:检查一次工具调用结果里有没有可用图片,有的话创建一条额外的“图片输出”历史记录。它最多只取第一张能成功解析的图片,避免一次结果刷出很多提示。

数据流:进去的是工具调用结果,可能成功也可能失败;它只处理成功结果,逐个查看内容块,尝试解码图片;出来的是可选的图片历史单元,找不到图片或解码失败就返回空。

调用关系McpToolCallCell::complete 在工具完成时调用它。它把每个内容块的具体图片解析交给 decode_mcp_image

调用图:被 1 处调用(complete)。

decode_mcp_image285–317 ↗
fn decode_mcp_image(block: &serde_json::Value) -> Option<DynamicImage>

作用:把 MCP 返回的图片内容块真正解成内存里的图片对象。它会处理普通 base64,也会处理 data: 开头的数据网址格式。

数据流:进去的是一个 JSON 内容块;它先确认这块确实是图片,再取出 base64 字符串,解码成原始字节,猜测图片格式,最后交给图片库解析;出来的是可选图片对象,任何一步失败都会返回空并记录错误日志。

调用关系:它是图片检测流程里的底层小工。try_new_completed_mcp_tool_call_with_image_output 会用它逐块尝试,成功后才会生成图片提示历史单元。

调用图:调用 1 个内部函数(new);外部调用 2 个(new, clone)。

empty_mcp_output319–339 ↗
fn empty_mcp_output() -> PlainHistoryCell

作用:生成 /mcp 命令在没有配置任何 MCP 服务器时的提示内容。它会告诉用户当前没有服务器,并给出文档链接。

数据流:进去不需要参数;它组装几行带样式的终端文字,包括标题、空状态说明和 MCP 文档链接;出来的是一个普通历史单元。

调用关系:当用户查看 MCP 工具但系统发现没有服务器配置时,界面会使用它来给出友好的空状态,而不是只显示空白。

调用图:调用 1 个内部函数(new);外部调用 1 个(vec!)。

new_mcp_tools_output343–513 ↗
fn new_mcp_tools_output(
    config: &Config,
    tools: HashMap<String, codex_protocol::mcp::Tool>,
    resources: HashMap<String, Vec<Resource>>,
    resource_templates: HashMap<String, Vec<Resource

作用:根据本地配置和已发现的工具,生成 /mcp 工具清单。注意这个函数带有测试编译标记,主要在测试环境中使用。

数据流:进去的是配置、工具表、资源表、资源模板表和认证状态表;它按服务器名排序,逐个列出是否启用、认证方式、启动命令或网址、工具名、资源和模板;出来的是一个普通历史单元。

调用关系:它的布局和正式的状态版输出保持一致,方便测试本地 MCP 管理器看到的内容是否会被正确展示。它会用 mcp_auth_status_label 把认证状态变成文字。

调用图:外部调用 4 个(from, new, format!, vec!)。

new_mcp_tools_output_from_statuses524–616 ↗
fn new_mcp_tools_output_from_statuses(
    statuses: &[McpServerStatus],
    detail: McpServerStatusDetail,
) -> PlainHistoryCell

作用:根据应用服务器返回的 MCP 服务器状态,生成用户看到的 /mcp 工具清单。这是远端状态的展示版,不再额外混入本地配置。

数据流:进去的是一组服务器状态和一个详情开关;它先按服务器名排序,再列出认证状态和工具名;如果详情开关是完整模式,还会列出资源和资源模板;出来的是一个普通历史单元。

调用关系:当 TUI 从应用服务器拿到 MCP 状态响应后,会用它渲染清单。它也用 mcp_auth_status_label 做认证状态文字化,并用详情开关决定是否显示更多内容。

调用图:外部调用 6 个(from, iter, sort_by, format!, matches!, vec!)。

McpInventoryLoadingCell::new631–636 ↗
fn new(animations_enabled: bool) -> Self

作用:创建一条“正在加载 MCP 清单”的临时历史单元。它会记住开始时间,这样后面能显示动画。

数据流:进去的是动画开关;它记录当前时间和开关值;出来的是新的加载单元,里面还没有实际清单内容。

调用关系new_mcp_inventory_loading 会调用它。通常在用户触发 /mcp 后、服务器状态还没返回前使用。

调用图:被 1 处调用(new_mcp_inventory_loading);外部调用 1 个(now)。

McpInventoryLoadingCell::display_lines640–655 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>

作用:把“正在加载 MCP 清单”显示成终端里的一行提示,可能带一个会动的小符号。

数据流:进去的是加载单元和屏幕宽度;它读取开始时间和动画开关,生成活动指示器,再拼上 Loading MCP inventory…;出来的是一行带样式的显示内容,不改状态。

调用关系:清单请求还在路上时,聊天界面重绘会调用它。请求完成后,这个临时单元会被真正的清单输出替换或清掉。

调用图:外部调用 1 个(vec!)。

McpInventoryLoadingCell::raw_lines657–659 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成加载 MCP 清单时的纯文本提示。它不包含动画,只保留一句明确说明。

数据流:进去的是加载单元;它直接生成 Loading MCP inventory... 这一行;出来的是纯文本行列表,不改状态。

调用关系:它是 McpInventoryLoadingCell::display_lines 的纯文本配套版本,用于日志或转录内容。

调用图:外部调用 1 个(vec!)。

McpInventoryLoadingCell::transcript_animation_tick661–666 ↗
fn transcript_animation_tick(&self) -> Option<u64>

作用:告诉界面加载提示是否需要继续刷新动画。动画关闭时它会明确说“不需要”。

数据流:进去的是加载单元;如果动画关闭,返回空;如果动画开启,就根据经过时间每 50 毫秒算一个刻度;出来的是可选数字,不改状态。

调用关系:聊天历史的动画刷新机制会读取它,然后重新调用 display_lines,让加载符号看起来在动。

调用图:外部调用 1 个(elapsed)。

new_mcp_inventory_loading670–672 ↗
fn new_mcp_inventory_loading(animations_enabled: bool) -> McpInventoryLoadingCell

作用:提供一个方便入口,用来创建 MCP 清单加载中的提示单元。外部代码只需要传动画开关即可。

数据流:进去的是是否开启动画;它把参数交给 McpInventoryLoadingCell::new;出来的是新的加载单元。

调用关系:当用户请求查看 MCP 清单、但状态还没拿到时,外层聊天界面会调用它来放入临时提示。

调用图:调用 1 个内部函数(new)。

format_mcp_invocation673–692 ↗
fn format_mcp_invocation(invocation: McpInvocation) -> Line<'a>

作用:把一次 MCP 调用格式化成短短一行,比如 server.tool({参数})。这让用户能快速看出到底调用了哪个服务器的哪个工具。

数据流:进去的是服务器名、工具名和可选 JSON 参数;它把参数压成紧凑 JSON 字符串,再把服务器名和工具名加上颜色、参数变暗;出来的是一行可显示的终端内容。

调用关系McpToolCallCell::display_lines 会用它生成工具调用标题。这样标题格式集中在一个地方,后续显示时只管折行和排版。

调用图:被 1 处调用(display_lines);外部调用 1 个(vec!)。

tui/src/history_cell/notices.rs源码 ↗
domain_logiccross-cutting

这个文件把几类系统提示包装成统一的“历史单元”。可以把它想成聊天记录里的小公告栏:有的告诉你软件有新版本,有的提醒请求可能有安全风险,有的说某功能快不能用了,还有普通信息、警告和错误。它不负责判断什么时候该提示,而是负责把已经决定好的提示,变成终端里好看的多行文字:带颜色、图标、边框、自动换行,也能生成不带装饰的纯文本版本,方便复制、保存或做会话记录。比较重要的一点是,带网址的提示还会额外标注成终端可点击链接。这样同一条消息既能在屏幕上醒目显示,也能在日志里保持清楚可读。

函数细节16
UpdateAvailableHistoryCell::new14–19 ↗
fn new(latest_version: String, update_action: Option<UpdateAction>) -> Self

作用:创建一张“有新版本可用”的提示卡片。调用方把最新版本号和可选的更新命令交给它,它保存起来,之后用于显示给用户看。

数据流:进去的是最新版本号,以及可能存在的更新动作;它把这两样东西放进 UpdateAvailableHistoryCell 这个小容器里;出来的是一个可以放进历史记录里的更新提醒对象,不会立刻显示,只是先备好材料。

调用关系:程序运行时检测到新版本后会用它生成提示;快照测试也会用它固定检查显示效果。后面的显示函数会读取它保存的版本号和更新动作,真正把内容画出来。

调用图:被 3 处调用(run, standalone_unix_update_available_history_cell_snapshot, standalone_windows_update_available_history_cell_snapshot)。

UpdateAvailableHistoryCell::display_lines23–57 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把“有新版本”的提醒变成终端里带颜色、带边框、会自动换行的显示行。它让用户一眼看到当前版本、最新版本、该运行什么命令,以及去哪里看发布说明。

数据流:进去的是终端可用宽度,以及对象里保存的最新版本号和更新动作;它先决定显示“运行某命令更新”还是“去官网看安装方式”,再拼出带图标、颜色、链接样式的文字,并按宽度换行、加边框;出来的是一组可以直接交给终端界面渲染的行。

调用关系:它是更新提醒真正上屏的核心步骤。display_hyperlink_lines 会先调用它拿到普通显示行,然后再给其中的网址加上可点击链接信息。

调用图:调用 1 个内部函数(new);被 1 处调用(display_hyperlink_lines);外部调用 3 个(line!, text!, from)。

UpdateAvailableHistoryCell::raw_lines59–73 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成“有新版本”提醒的纯文本版本。它用于不需要颜色、边框和终端样式的场景,比如复制、日志或转录记录。

数据流:进去的是对象里保存的最新版本号和更新动作;它把这些内容拼成普通文字行,包括版本变化、更新方法、发布说明地址;出来的是一组没有花样装饰的文本行,也不会改动对象本身。

调用关系:它和 display_lines 是同一条提醒的两种出口:display_lines 给人看屏幕,raw_lines 给记录和纯文本场景用。它不依赖其他本文件函数。

调用图:外部调用 2 个(format!, vec!)。

new_warning_event84–86 ↗
fn new_warning_event(message: String) -> PrefixedWrappedHistoryCell

作用:创建一条黄色警告消息。它适合显示“要注意,但不一定是致命错误”的情况。

数据流:进去的是一段警告文字;它把文字染成黄色,加上黄色的警告图标前缀,并设置换行后对齐用的空白前缀;出来的是一个带前缀、会自动换行的历史单元。

调用关系:上层流程遇到需要提醒用户小心的事件时会调用它。它把具体排版交给 PrefixedWrappedHistoryCell::new,让通用的“带前缀换行文本”组件负责后续显示。

调用图:调用 1 个内部函数(new)。

new_cyber_policy_error_event93–95 ↗
fn new_cyber_policy_error_event() -> CyberPolicyNoticeCell

作用:创建一张网络安全政策提示卡片。它用于告诉用户:这次聊天被系统认为可能涉及网络安全风险。

数据流:进去不需要额外参数;它直接生成一个 CyberPolicyNoticeCell;出来的是固定内容的政策提示对象,后面显示时才会生成具体文字和链接。

调用关系:当上层判断需要给用户展示网络安全政策说明时,会调用它。它只是把对应的提示类型交出去,真正的文字排版由 CyberPolicyNoticeCell 的显示函数完成。

CyberPolicyNoticeCell::display_lines98–129 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把网络安全政策提示变成终端里可读、醒目的多行文字。它告诉用户为什么被拦下、可以尝试改写请求,以及如何加入授权项目。

数据流:进去的是终端宽度;它先放一行蓝色信息图标和标题,再把说明文字按宽度换行,最后附上 Trusted Access for Cyber 的网址;出来的是可直接显示在终端里的多行内容。

调用关系:它是网络安全政策提示上屏的核心。display_hyperlink_lines 会调用它生成文字,然后再给里面的网址加可点击链接。

调用图:调用 1 个内部函数(new);被 1 处调用(display_hyperlink_lines);外部调用 3 个(from, new, vec!)。

CyberPolicyNoticeCell::raw_lines131–139 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成网络安全政策提示的纯文本版本。它适合放进日志、复制内容或会话转录中,不带颜色和终端样式。

数据流:进去的是这个固定提示对象;它输出标题、说明文字和项目网址三行普通文本;出来的是干净的文本行,不会改变任何状态。

调用关系:它和 display_lines 对应:一个给终端视觉显示,一个给纯文本记录。因为提示内容固定,它不需要调用其他本文件函数。

调用图:外部调用 1 个(vec!)。

new_deprecation_notice156–161 ↗
fn new_deprecation_notice(
    summary: String,
    details: Option<String>,
) -> DeprecationNoticeCell

作用:创建一张“功能将被废弃或不再推荐使用”的提醒卡片。它用于提前告诉用户某个用法快要变了,避免用户以后突然踩坑。

数据流:进去的是一句摘要,以及可选的详细说明;它把这两部分保存到 DeprecationNoticeCell 里;出来的是一个之后可以显示或写入记录的废弃提醒对象。

调用关系:上层发现用户触发了旧功能、旧配置或旧行为时会调用它。后续由 DeprecationNoticeCell::display_lines 和 raw_lines 分别生成屏幕版和纯文本版。

DeprecationNoticeCell::display_lines164–177 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把废弃提醒显示成终端里的红色警告。摘要会很醒目,详细说明如果存在则会自动换行显示在下面。

数据流:进去的是终端宽度,以及对象里保存的摘要和可选详情;它先生成带红色警告图标的摘要行,再按宽度处理详情文字;出来的是一组适合终端显示的行。

调用关系:它是废弃提醒的屏幕显示出口。new_deprecation_notice 先创建对象,等历史记录需要绘制时再调用这个函数生成实际可见内容。

调用图:调用 1 个内部函数(new);外部调用 3 个(from, new, vec!)。

DeprecationNoticeCell::raw_lines179–185 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成废弃提醒的纯文本版本。它让日志或复制内容里也能看到摘要和详情,但不包含颜色、图标强调等终端装饰。

数据流:进去的是对象里的摘要和可选详情;它先放入摘要,如果有详情,就把详情按原始文本拆成多行加入;出来的是普通文本行列表。

调用关系:它和 display_lines 配套使用:一个给屏幕,一个给记录。它会借助通用的原始文本拆行工具处理详情,保证多行说明不会挤成一行。

调用图:外部调用 1 个(vec!)。

new_info_event187–195 ↗
fn new_info_event(message: String, hint: Option<String>) -> PlainHistoryCell

作用:创建一条普通信息提示。它适合显示轻量通知,比如“某件事已经完成”,也可以附一小段灰色提示作为补充。

数据流:进去的是主消息,以及可选的提示文字;它生成一行以小圆点开头的文本,如果有提示就接在后面并用更淡的颜色显示;出来的是一个简单的 PlainHistoryCell。

调用关系:上层流程需要插入普通提示时会调用它。它不再交给复杂组件处理,因为这种信息只有一行,直接包装成 PlainHistoryCell 就够了。

调用图:外部调用 1 个(vec!)。

new_error_event197–203 ↗
fn new_error_event(message: String) -> PlainHistoryCell

作用:创建一条红色错误消息。它用于把失败或异常情况清楚地显示给用户。

数据流:进去的是错误文字;它在前面加上方块符号,并整体染成红色;出来的是一个包含单行错误内容的 PlainHistoryCell。

调用关系:当上层流程需要把错误写进聊天历史时会调用它。它直接生成 PlainHistoryCell,不做复杂换行或链接处理,因为错误提示通常要短而醒目。

调用图:外部调用 1 个(vec!)。

tui/src/history_cell/patches.rs源码 ↗
domain_logicmain loop / history rendering

在这个终端界面里,很多动作都会留下历史记录,比如模型提议修改文件、补丁没打上、用户查看图片、工具生成图片。这个文件就像一个“记录排版员”:它不负责真的改文件或生成图片,只负责把这些结果变成好读的几行文字。PatchHistoryCell 保存一批文件改动和当前目录,然后在显示时把它们做成差异摘要,也就是“哪个文件新增、修改、删除”的列表。遇到补丁应用失败时,它会做出醒目的失败标题,并把错误输出按工具调用的格式附在下面。图片相关的函数则会显示用户看了哪张图、生成图片是否成功、保存到了哪里。一个重要点是,它会尽量把路径显示成相对当前目录的友好形式,避免用户看到又长又难读的系统路径。

函数细节6
PatchHistoryCell::display_lines12–14 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:这个函数把一次补丁里的文件变化,转换成终端界面当前宽度下适合显示的几行摘要。用户看到的“改了哪些文件”的列表,主要就是它准备出来的。

数据流:进去的是界面宽度,以及这个对象里已经保存的文件改动和当前目录 → 它把这些信息交给差异摘要生成器,让路径和文字按宽度排好 → 出来的是一组可以直接画到终端上的文字行,不会改动原始改动数据。

调用关系:当历史记录需要显示到屏幕上时,界面框架会通过 HistoryCell 这个统一接口调用它。它自己不拼每个细节,而是把核心排版工作交给 create_diff_summary,让补丁摘要和其他地方保持同一种样式。

PatchHistoryCell::raw_lines16–22 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:这个函数生成一份“纯文本版”的补丁摘要,适合复制、导出或在不需要终端花样样式的地方使用。它和 display_lines 看的是同一批改动,只是输出更朴素。

数据流:进去的是对象里保存的文件改动和当前目录 → 它用固定的原始摘要宽度生成差异摘要,再把带样式的行转成普通文字行 → 出来的是一组更适合原样保存或复制的文本行,不改变对象内容。

调用关系:当系统需要拿到历史记录的原始文本,而不是屏幕上的自适应显示版本时,会走这个函数。它同样依赖 create_diff_summary 生成主体内容,再通过 plain_lines 去掉或简化显示样式。

new_patch_event27–35 ↗
fn new_patch_event(
    changes: HashMap<PathBuf, FileChange>,
    cwd: &Path,
) -> PatchHistoryCell

作用:这个函数用来创建一条“有补丁待展示”的历史记录。调用者把文件改动清单和当前目录交给它,它就包装成 PatchHistoryCell,之后界面就能显示这次改动摘要。

数据流:进去的是一张文件路径到文件变化的表,以及当前工作目录 → 它把当前目录复制成这个历史记录自己保存的一份路径,连同改动表一起放进 PatchHistoryCell → 出来的是一个新的补丁历史单元,供后续显示使用。

调用关系:当上游流程拿到一批拟议文件改动后,会调用它来生成历史列表中的一个节点。它只做打包,不做展示;真正显示时,会由 PatchHistoryCell::display_lines 或 PatchHistoryCell::raw_lines 接着把内容排成文字。

调用图:外部调用 1 个(to_path_buf)。

new_patch_apply_failure37–61 ↗
fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell

作用:这个函数创建一条“补丁应用失败”的历史记录。它会先放一个醒目的失败标题,再把错误信息整理成用户能读的几行输出。

数据流:进去的是补丁工具返回的错误文本 stderr → 它先创建一个空的显示行列表,加入“Failed to apply patch”这样的失败标题;如果错误文本不是空的,就把错误包装成一次失败的命令输出,并按行数限制和错误输出格式整理 → 出来的是一个 PlainHistoryCell,里面装着可以直接显示的失败说明。

调用关系:当系统尝试应用补丁但失败时,会用它把底层错误变成历史记录。它把错误排版这件事交给 output_lines,因为工具输出可能很长,需要统一限制行数、加前缀、只显示错误部分。

调用图:外部调用 3 个(from, new, new)。

new_view_image_tool_call63–72 ↗
fn new_view_image_tool_call(path: AbsolutePathBuf, cwd: &Path) -> PlainHistoryCell

作用:这个函数创建一条“用户查看了某张图片”的历史记录。它让历史列表里能清楚显示:刚才打开或查看的是哪一个图片文件。

数据流:进去的是图片的绝对路径和当前工作目录 → 它先把绝对路径转换成更适合给人看的路径,比如相对当前目录的路径;然后拼出两行文字,一行说明查看了图片,一行显示路径 → 出来的是一个 PlainHistoryCell,供界面直接显示。

调用关系:当图片查看工具被调用后,系统会用这个函数留下记录。它依赖 display_path_for 来把机器用的完整路径变成用户更容易认的路径,然后把结果放进普通历史单元。

调用图:调用 1 个内部函数(as_path);外部调用 1 个(vec!)。

new_image_generation_call74–95 ↗
fn new_image_generation_call(
    call_id: String,
    status: &str,
    revised_prompt: Option<String>,
    saved_path: Option<AbsolutePathBuf>,
) -> PlainHistoryCell

作用:这个函数创建一条“图片生成工具被调用”的历史记录。它既能表示生成成功,也能表示生成失败,还能显示提示词或保存位置。

数据流:进去的是调用编号、状态文字、可能被修订过的提示词、以及可能存在的保存路径 → 它先决定详情文字:有修订提示词就显示提示词,没有就显示调用编号;再根据状态选择成功或失败标题;如果有保存路径,就尽量把本地文件路径转成 file:// 这种可点击的文件网址,失败时再退回普通路径文字 → 出来的是一个 PlainHistoryCell,包含生成结果、详情和保存位置。

调用关系:当图片生成工具完成或失败后,系统会调用它把结果写入历史。它不负责真正生成图片,只把工具返回的信息整理给用户看;路径转换交给 Url::from_file_path,这样界面里可能更容易打开保存的文件。

调用图:外部调用 2 个(from_file_path, vec!)。

tui/src/history_cell/request_user_input.rs源码 ↗
domain_logichistory rendering

这个文件专门负责显示已经结束的 request_user_input 交互记录。可以把它想成一张“问答回执单”:上面先写总共有几个问题、答了几个;然后逐条列出问题和答案。如果某个问题没有答案,它会标成未回答;如果整个过程被打断,还会额外提醒还有多少题没答。它还特别照顾两件事:一是终端窗口可能很窄,所以长问题和长答案会自动换行,并在每一行前面加合适的缩进;二是有些问题是秘密内容,比如密码,这类答案只显示成星号或圆点,不泄露真实内容。文件里主要有一个历史记录单元 RequestUserInputResultCell,以及两个小帮手:一个负责“带前缀换行”,一个负责把答案拆成“选项”和“用户补充说明”。

函数细节4
RequestUserInputResultCell::display_lines14–109 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:这个函数生成给终端界面直接显示用的彩色、多样式文本行。它会把问题、答案、未回答状态和中断提示整理成一段好读的历史记录。

数据流:进去的是这个单元里保存的问题列表、答案表、中断标记,以及当前界面宽度 → 它先统计一共多少题、答了多少题,再给每个问题生成带项目符号和缩进的显示行;遇到秘密问题就把答案遮住,遇到长文本就调用换行帮手,遇到带补充说明的答案就把说明单独标出来 → 出来的是一组 Line,也就是终端界面可以逐行绘制的内容;它不改动原始问题和答案。

调用关系:它是 HistoryCell 这类历史记录单元在需要“画到屏幕上”时会被调用的主要入口。它自己负责组织整段显示内容,中途把长文本排版工作交给 wrap_with_prefix,把答案拆分工作交给 split_request_user_input_answer。

调用图:调用 2 个内部函数(split_request_user_input_answer, wrap_with_prefix);外部调用 3 个(default, format!, vec!)。

RequestUserInputResultCell::raw_lines111–151 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:这个函数生成不带复杂样式的纯文本版本。它适合复制、导出、搜索,或者在不需要彩色终端效果的地方使用。

数据流:进去的是同一份问题、答案和中断状态 → 它统计答题进度,逐个写出问题;有答案就写 answer 或 note,没有答案就写未回答;秘密答案会被替换成星号 → 出来的是一组简单的 Line 文本行;同样不会改变原始数据。

调用关系:它和 display_lines 是同一份历史内容的两种输出方式:display_lines 面向漂亮的界面显示,raw_lines 面向朴素文本。它在处理普通答案时也会调用 split_request_user_input_answer,确保选项和补充说明的拆法一致。

调用图:调用 1 个内部函数(split_request_user_input_answer);外部调用 3 个(from, format!, vec!)。

wrap_with_prefix155–170 ↗
fn wrap_with_prefix(
    text: &str,
    width: usize,
    initial_prefix: Span<'static>,
    subsequent_prefix: Span<'static>,
    style: Style,
) -> Vec<Line<'static>>

作用:这个小工具负责把一段普通文字按终端宽度自动折行,并给第一行和后续行加上不同前缀。这样长问题或长答案不会挤出屏幕,也能保持整齐缩进。

数据流:进去的是原始文字、可用宽度、第一行前缀、后续行前缀,以及文字样式 → 它先把文字做成带样式的行,再交给自适应换行工具按宽度切成多行,最后把结果整理成拥有自己内容的 Line 列表 → 出来的是已经换好行、带好缩进和样式的显示行。

调用关系:它是 display_lines 的排版帮手。display_lines 每次需要显示可能很长的问题、答案、备注或中断总结时,都会把具体文本交给它,让它负责“别超宽、排整齐”。

调用图:调用 1 个内部函数(new);被 1 处调用(display_lines);外部调用 3 个(from, new, vec!)。

split_request_user_input_answer174–187 ↗
fn split_request_user_input_answer(
    answer: &ToolRequestUserInputAnswer,
) -> (Vec<String>, Option<String>)

作用:这个函数把一个用户回答拆成两类:正式选择的答案,以及可选的自由备注。因为备注在数据里是用特定文字前缀 user_note: 标出来的,所以需要这里统一识别。

数据流:进去的是一个回答对象,里面有多条 answer 字符串 → 它逐条查看:如果某条以 user_note: 开头,就去掉这个标记并当作备注;否则就当作普通选项答案保存 → 出来的是一个选项列表和一个可能存在的备注;原回答不会被修改。

调用关系:它同时服务 display_lines 和 raw_lines。也就是说,不管是彩色界面版还是纯文本版,都用同一套规则理解用户答案,避免一个地方把备注当答案、另一个地方又单独显示备注的混乱。

调用图:被 2 处调用(display_lines, raw_lines);外部调用 1 个(new)。

tui/src/history_cell/search.rs源码 ↗
domain_logicmain loop / history rendering

这个文件像聊天记录里的“网页搜索小卡片”。当程序开始联网搜索时,它先创建一个未完成的 WebSearchCell,界面上显示“Searching the web”,旁边可能有活动中的小圆点动画;等搜索结束后,它变成“Searched the web”,小圆点也变成静态。文件还会把不同搜索动作翻译成人能读的文字:比如搜索关键词、打开网页地址、在网页里查找某个词。这样用户不用看内部调用编号或复杂数据,只看到一句清楚的历史说明。它同时提供两种输出:一种是带样式、会按宽度换行的界面文本;另一种是纯文本,方便复制、日志或无样式场景使用。

函数细节11
web_search_header5–11 ↗
fn web_search_header(completed: bool) -> &'static str

作用:根据网页搜索是否已经结束,选一句标题文字。没结束时显示“正在搜索网页”,结束后显示“已搜索网页”。

数据流:输入是一个 completed 标记,表示任务完没完成;函数只检查这个真假值;输出固定的英文标题字符串,不改动任何状态。

调用关系:它是显示文字的第一块积木。WebSearchCell::display_lines 和 WebSearchCell::raw_lines 在生成界面文字或纯文字时都会先问它:现在该写“Searching the web”还是“Searched the web”。

调用图:被 2 处调用(display_lines, raw_lines)。

web_search_action_detail13–38 ↗
fn web_search_action_detail(action: &WebSearchAction) -> String

作用:把具体的网页搜索动作,整理成用户能看懂的细节文字。比如搜索词、网页地址,或者“在某个网页里找某个词”。

数据流:输入是一个 WebSearchAction,也就是“搜索、打开网页、页内查找、其他”这类动作;函数按动作类型挑出最有用的信息,比如 query、queries、url、pattern;输出一段说明文字,缺信息时就输出空字符串。

调用关系:它是把内部动作数据翻译成人话的地方。web_search_detail 会借它先尝试拿到动作细节;遇到多个搜索词时,它会用格式化文字做一个简短预览。

调用图:外部调用 2 个(new, format!)。

web_search_detail40–47 ↗
fn web_search_detail(action: Option<&WebSearchAction>, query: &str) -> String

作用:决定搜索记录后面应该跟哪段细节文字。优先用动作里的详细信息;如果动作里没东西,就退回使用原始 query。

数据流:输入是一份可有可无的 action,以及一个 query 字符串;它先从 action 里提取细节,提不到或为空时就用 query;输出最终要显示的详情文字。

调用关系:它夹在底层动作解释和最终显示之间。WebSearchCell::display_lines 和 WebSearchCell::raw_lines 都靠它拿到同一份详情,保证带样式界面和纯文本内容说的是同一件事。

调用图:被 2 处调用(display_lines, raw_lines)。

WebSearchCell::new60–74 ↗
fn new(
        call_id: String,
        query: String,
        action: Option<WebSearchAction>,
        animations_enabled: bool,
    ) -> Self

作用:新建一张网页搜索历史卡片,记录调用编号、查询内容、动作、开始时间和动画设置。通常在一次搜索刚出现时调用。

数据流:输入是 call_id、query、可选 action、以及是否开启动画;函数记录当前时间作为开始时间,把 completed 设为 false;输出一个新的 WebSearchCell,不会自动标记完成。

调用关系:它是创建卡片的基础入口。new_active_web_search_call 用它创建“正在搜索”的卡片,new_web_search_call 也先用它创建卡片,然后再把卡片标成完成。

调用图:被 2 处调用(new_active_web_search_call, new_web_search_call);外部调用 1 个(now)。

WebSearchCell::call_id76–78 ↗
fn call_id(&self) -> &str

作用:取出这张搜索卡片对应的调用编号。外部可以用这个编号把后续更新和正确的历史记录对上。

数据流:输入是这张 WebSearchCell 自己;函数读取内部 call_id;输出这个编号的字符串引用,不复制也不修改卡片。

调用关系:它像卡片上的编号标签。周围的历史记录系统需要识别哪一次网页搜索正在被更新时,会用这个方法读取编号。

WebSearchCell::update80–83 ↗
fn update(&mut self, action: WebSearchAction, query: String)

作用:更新一张还在变化的网页搜索卡片,把新的动作和新的查询内容放进去。适合搜索过程中收到更准确信息时使用。

数据流:输入是新的 WebSearchAction 和新的 query;函数把旧的 action 替换成新 action,把旧 query 替换成新 query;输出没有返回值,但这张卡片之后显示的文字会变。

调用关系:它负责让同一张历史卡片跟上搜索进展。外部流程拿到新的搜索动作后,会调用它改内容;之后 WebSearchCell::display_lines 或 WebSearchCell::raw_lines 再显示时,就会读到更新后的信息。

WebSearchCell::complete85–87 ↗
fn complete(&mut self)

作用:把网页搜索卡片标记为已经完成。完成后标题和小圆点样式都会变成静态的“已完成”状态。

数据流:输入是这张 WebSearchCell 自己;函数把 completed 从 false 改成 true;没有返回值,但会影响后续显示出来的标题、分隔词和图标。

调用关系:它是搜索结束时按下的“完成按钮”。new_web_search_call 会创建卡片后立刻调用它;运行中的搜索结束时,外部也可以调用它让显示从“Searching”变成“Searched”。

WebSearchCell::display_lines91–111 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成真正给终端界面显示的、带样式并能按宽度换行的文本行。它会处理粗体标题、搜索细节、前面的圆点或动画指示。

数据流:输入是可用显示宽度 width,以及这张卡片里的完成状态、开始时间、动画开关、动作和 query;函数拼出标题和详情,选择活动小圆点或静态小圆点,再交给带前缀的换行组件;输出一组 Line,供界面绘制。

调用关系:它是这张卡片在 TUI(终端用户界面,也就是在命令行窗口里的界面)里的正式出口。它会向 web_search_header 要标题,向 web_search_detail 要详情,并使用动画设置决定搜索中状态怎么显示。

调用图:调用 4 个内部函数(new, web_search_detail, web_search_header, from_animations_enabled);外部调用 2 个(from, vec!)。

WebSearchCell::raw_lines113–122 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成不带颜色、不带粗体、不带动画的纯文本版本。适合日志、复制内容,或任何不需要终端样式的地方。

数据流:输入是这张卡片当前保存的状态和内容;函数取标题、取详情,再按是否完成选择连接方式;输出一个只包含普通文字的 Line 列表,不改变卡片。

调用关系:它和 WebSearchCell::display_lines 是同一内容的两个出口:display_lines 给人看的界面用,raw_lines 给纯文本场景用。两者都依赖 web_search_header 和 web_search_detail,所以文字含义保持一致。

调用图:调用 2 个内部函数(web_search_detail, web_search_header);外部调用 1 个(vec!)。

new_active_web_search_call125–131 ↗
fn new_active_web_search_call(
    call_id: String,
    query: String,
    animations_enabled: bool,
) -> WebSearchCell

作用:创建一张“正在进行中”的网页搜索卡片。调用它时,搜索还没结束,所以卡片会保留动画设置并显示进行中状态。

数据流:输入是调用编号 call_id、初始 query、以及是否开启动画;函数把 action 设为空,调用 WebSearchCell::new 创建未完成卡片;输出这个 WebSearchCell。

调用关系:它是开始搜索时用的便捷入口。它把创建细节包起来,真正建对象的活交给 WebSearchCell::new,方便外部流程不用自己填写那些默认值。

调用图:调用 1 个内部函数(new)。

new_web_search_call133–146 ↗
fn new_web_search_call(
    call_id: String,
    query: String,
    action: WebSearchAction,
) -> WebSearchCell

作用:创建一张已经完成的网页搜索卡片。适合把一个已知结果直接放进历史记录里。

数据流:输入是 call_id、query 和具体 action;函数先用 WebSearchCell::new 创建卡片,并关闭动画,然后调用 complete 把它标记为完成;输出完成状态的 WebSearchCell。

调用关系:它是记录已完成搜索的便捷入口。它复用 WebSearchCell::new 做初始化,再马上通过 WebSearchCell::complete 进入完成状态,这样显示时会直接写“Searched the web”。

调用图:调用 1 个内部函数(new)。

tui/src/history_cell/hook_cell.rs源码 ↗
domain_logic主界面运行中,收到 hook 开始/结束事件、定时刷新界面、生成历史 transcript 时活跃

hook 可以理解成系统在某些动作前后自动跑的小检查或小脚本。它们通常不是用户主动点开的工具,所以这个文件让它们在界面上更“安静”:刚开始运行时先藏 300 毫秒,避免一闪而过;如果运行久了才显示“正在运行”;如果它安静地成功了,就短暂停留一下再消失;如果失败、有警告、有错误或有输出,就变成正式历史记录。核心结构是 HookCell,里面放多个 HookRunCell。每个 HookRunCell 用 HookRunState 记录自己处在“等待显示、正在显示、成功后短暂停留、已完成保留”哪一步。显示时,相邻且同类的运行中 hook 会合成一行,像把几张相同的小纸条叠成一张,减少刷屏。

函数细节60
HookCell::new_active119–126 ↗
fn new_active(run: HookRunSummary, animations_enabled: bool) -> Self

作用:新建一个包含“刚开始运行的 hook”的显示单元。调用它的人通常刚收到 hook 开始事件,需要先把这次运行记下来。

数据流:输入一份 hook 运行摘要和是否开启动画的设置 → 创建空的 HookCell → 把这次运行作为待显示状态放进去 → 输出一个可继续更新、可渲染的 HookCell。

调用关系:它是创建活动 hook 单元的内部入口,被 new_active_hook_cell 和几个测试使用;真正登记运行的活交给 HookCell::start_run。

调用图:被 5 处调用(new_active_hook_cell, pending_hook_does_not_animate_transcript, visible_hook_animates_transcript_when_animations_enabled, visible_hook_does_not_animate_transcript_when_animations_disabled, visible_hook_without_animations_omits_spinner);外部调用 1 个(new)。

HookCell::new_completed129–136 ↗
fn new_completed(run: HookRunSummary, animations_enabled: bool) -> Self

作用:新建一个包含“已经完成的 hook”的显示单元。它用于从历史记录或恢复数据里直接重建界面,不经过实时运行过程。

数据流:输入一份已完成 hook 的摘要和动画设置 → 创建空 HookCell → 尝试把这次完成结果加入进去 → 输出一个历史用的 HookCell;安静成功的 hook 会被忽略。

调用关系:它被 new_completed_hook_cell 和测试辅助函数 tests::completed_hook_cell 使用;实际筛选是否保留由 HookCell::add_completed_run 完成。

调用图:被 2 处调用(new_completed_hook_cell, completed_hook_cell);外部调用 1 个(new)。

HookCell::is_empty138–140 ↗
fn is_empty(&self) -> bool

作用:判断这个 hook 单元里是否已经没有任何运行记录。它用来决定这个单元还能不能留在界面或历史里。

数据流:读取 HookCell 内部的 runs 列表 → 检查列表是否为空 → 返回 true 或 false,不改动任何内容。

调用关系:它被 HookCell::should_flush 调用,作为判断“是否可以从活动槽位清出去”的一部分。

调用图:被 1 处调用(should_flush)。

HookCell::is_active143–145 ↗
fn is_active(&self) -> bool

作用:判断这个单元里是否还有会继续变化的 hook。比如还在等结束事件,或者还在等短暂停留时间结束。

数据流:读取所有 run 的状态 → 只要有一个状态仍然活跃就返回 true → 如果全都稳定完成则返回 false。

调用关系:它被 HookCell::should_flush 使用;具体每个状态是否活跃由 HookRunState::is_active 判断。

调用图:被 1 处调用(should_flush)。

HookCell::should_flush148–150 ↗
fn should_flush(&self) -> bool

作用:判断这个 hook 单元是否该从“活动区域”移走。意思是:里面还有内容,但已经不会再变化了。

数据流:读取当前单元 → 先看是否还有活跃状态,再看是否为空 → 如果“不活跃且不为空”就返回 true。

调用关系:它串起 HookCell::is_active 和 HookCell::is_empty,供外层界面决定何时把完成内容转入普通历史或清理活动位置。

调用图:调用 2 个内部函数(is_active, is_empty)。

HookCell::should_render153–155 ↗
fn should_render(&self) -> bool

作用:判断这个单元现在是否真的有东西需要画到屏幕上。刚开始但还没到显示时间的 hook 会返回 false。

数据流:读取所有 run 的状态 → 检查有没有状态说“我现在应该显示” → 返回是否需要占用界面空间。

调用关系:它帮助外层界面避免绘制空白或一闪而过的 hook;判断细节来自 HookRunState::should_render。

HookCell::take_completed_persistent_runs161–176 ↗
fn take_completed_persistent_runs(&mut self) -> Option<Self>

作用:把应该永久留在历史里的已完成 hook 从活动单元里拆出来。安静成功的 hook 不会被带走,因为它们本来就不该污染历史。

数据流:取出 runs 列表里的每个 run → 有失败、阻止、停止或输出内容的放进 completed → 其他留在 remaining → 当前单元只保留 remaining,并返回一个新的持久 HookCell,若没有可保留内容则返回空。

调用关系:它通常在活动 hook 结束后被外层历史管理流程调用;筛选依据来自 HookRunState::has_persistent_output。

调用图:外部调用 1 个(new)。

HookCell::has_visible_running_run179–181 ↗
fn has_visible_running_run(&self) -> bool

作用:判断当前是否有正在屏幕上显示的运行中 hook。外层界面可用它判断活动区域是否已经占了可见空间。

数据流:读取所有 run → 检查是否有运行中且已经过了隐藏期的状态 → 返回 true 或 false。

调用关系:它依赖 HookRunState::is_running_visible 的判断,通常给布局或刷新逻辑做参考。

HookCell::advance_time184–192 ↗
fn advance_time(&mut self, now: Instant) -> bool

作用:推进这个单元的时间状态。比如到点后把隐藏的 hook 显示出来,或者把短暂停留结束的安静成功 hook 移除。

数据流:输入当前时间 now → 遍历每个 run,让该显示的显示 → 删除停留时间已过的安静成功 run → 返回这次是否造成了界面变化。

调用关系:它由定时刷新流程调用;内部依次使用 HookRunState::reveal_if_due 和 HookRunState::quiet_linger_expired。

HookCell::start_run198–212 ↗
fn start_run(&mut self, run: HookRunSummary)

作用:登记一个 hook 开始运行。若同一个 id 的开始事件重复来了,它不会加第二条,而是刷新原来的信息。

数据流:输入 hook 摘要 → 读取当前时间 → 如果已有相同 id,就更新事件名、状态消息并重置为等待显示;如果没有,就追加一个新的 HookRunCell → 当前 HookCell 被改动,没有单独返回值。

调用关系:它被 HookCell::new_active 调用,也会被外层事件处理在收到 hook begin 时调用;新状态由 HookRunState::pending 创建。

调用图:调用 1 个内部函数(pending);外部调用 1 个(now)。

HookCell::complete_run218–243 ↗
fn complete_run(&mut self, run: HookRunSummary) -> bool

作用:把某个正在跟踪的 hook 标记为完成。它会特别照顾“没有输出的成功”:没显示过就直接消失,显示过就短暂停留。

数据流:输入完成后的 hook 摘要 → 按 id 找到原来的 run → 如果是安静成功,就尝试进入短暂停留,不需要就删除;否则更新事件名、状态消息和完成内容 → 返回是否找到了这次 run。

调用关系:外层收到 hook end 事件时会用它;它调用 hook_run_is_quiet_success 判断是否安静成功,并用 HookRunState::completed 保存需要保留的结果。

调用图:调用 2 个内部函数(completed, hook_run_is_quiet_success);外部调用 1 个(now)。

HookCell::add_completed_run248–266 ↗
fn add_completed_run(&mut self, run: HookRunSummary)

作用:直接加入一个已经完成的 hook。它适合加载历史记录时使用,而不是实时运行时使用。

数据流:输入完成摘要 → 如果是没有输出的成功就丢弃 → 否则拆出 id、事件名、状态、输出等字段 → 新增一个 Completed 状态的 HookRunCell。

调用关系:它被 HookCell::new_completed 调用;是否该丢弃由 hook_run_is_quiet_success 决定,完成状态由 HookRunState::completed 创建。

调用图:调用 2 个内部函数(completed, hook_run_is_quiet_success)。

HookCell::next_timer_deadline268–273 ↗
fn next_timer_deadline(&self) -> Option<Instant>

作用:找出下一次需要定时刷新界面的时间点。比如某个 hook 到点要显示,或短暂停留到点要消失。

数据流:读取所有 run → 收集每个状态自己的下一个截止时间 → 取最早的一个返回;如果没有定时事项就返回空。

调用关系:外层定时器可用它安排下一次重绘;每个 run 的截止时间来自 HookRunState::next_timer_deadline。

HookCell::expire_quiet_runs_now_for_test276–280 ↗
fn expire_quiet_runs_now_for_test(&mut self)

作用:测试专用:强制让所有安静成功的短暂停留立即过期。这样测试不用真的等时间流逝。

数据流:遍历所有 run → 对每个 run 调用测试用的立即过期方法 → 修改内部截止时间,没有返回值。

调用关系:只在测试编译时存在;把具体改时间的动作交给 HookRunCell::expire_quiet_linger_now_for_test。

HookCell::reveal_running_runs_now_for_test283–288 ↗
fn reveal_running_runs_now_for_test(&mut self)

作用:测试专用:强制让等待显示的运行中 hook 现在就可以显示。这样测试可以直接验证渲染结果。

数据流:读取当前时间 → 遍历所有 run → 把待显示状态的显示截止时间改成现在 → 没有返回值。

调用关系:只给测试使用;具体修改由 HookRunCell::reveal_running_now_for_test 完成。

调用图:外部调用 1 个(now)。

HookCell::reveal_running_runs_after_delayed_redraw_for_test291–296 ↗
fn reveal_running_runs_after_delayed_redraw_for_test(&mut self)

作用:测试专用:模拟“本该早就显示,但界面延迟刷新”的情况。它用来检查安静成功的最短可见时间规则。

数据流:读取当前时间 → 遍历所有 run → 把待显示的截止时间改成更早的时间 → 没有返回值。

调用关系:只在测试里使用;具体时间调整交给 HookRunCell::reveal_running_after_delayed_redraw_for_test。

调用图:外部调用 1 个(now)。

HookCell::display_lines301–340 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>

作用:把 hook 单元转换成终端上要显示的文字行。它还会把相邻、同类的运行中 hook 合并成一行,减少视觉噪音。

数据流:输入可用宽度但这里主要按内容生成行 → 遍历 runs,跳过暂不显示的 run → 对运行中 hook 尝试分组,对已完成 hook 单独输出标题和内容 → 返回一组 Line,也就是终端渲染用的文字行。

调用关系:它是显示文本的核心,被 HookCell::transcript_lines、HookCell::raw_lines 和 HookCell::render 调用;分组时用 RunningHookGroup::new、earliest_instant、push_running_hook_group 等辅助函数。

调用图:调用 4 个内部函数(new, earliest_instant, push_hook_line_separator, push_running_hook_group);被 3 处调用(raw_lines, render, transcript_lines);外部调用 1 个(new)。

HookCell::transcript_lines343–345 ↗
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成 transcript,也就是记录/抄本里使用的 hook 文本行。这里故意和屏幕显示保持一致。

数据流:输入宽度 → 直接调用 HookCell::display_lines → 返回同样的一组文字行。

调用关系:它实现 HistoryCell 接口;把具体生成工作交给 HookCell::display_lines,避免屏幕和记录显示不一致。

调用图:调用 1 个内部函数(display_lines)。

HookCell::raw_lines347–349 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成去掉样式后的纯文本行。它适合复制、比较或测试,因为不带颜色和动画样式。

数据流:先用最大宽度调用 HookCell::display_lines 得到带样式的行 → 交给 plain_lines 去样式 → 返回纯文本 Line 列表。

调用关系:它实现 HistoryCell 接口;依赖外部 plain_lines 做样式剥离。

调用图:调用 1 个内部函数(display_lines);外部调用 1 个(plain_lines)。

HookCell::transcript_animation_tick352–363 ↗
fn transcript_animation_tick(&self) -> Option<u64>

作用:给 transcript 动画提供一个粗略的“节拍编号”。如果没开动画,或者还没有可见的运行中 hook,就不需要节拍。

数据流:先检查动画开关 → 找到一个可见运行中 hook 的开始时间 → 算出已经运行多少毫秒 → 每 600 毫秒归为一个编号返回。

调用关系:它让外层缓存知道什么时候应该因为动画而刷新 transcript;只关心 HookRunState::is_running_visible 和 start_time。

HookCell::render367–371 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:真正把这个 HookCell 画到终端缓冲区里。缓冲区可以理解成屏幕刷新前的一张草稿纸。

数据流:输入绘制区域和缓冲区 → 先生成显示行 → 包成 Paragraph 段落并设置不裁剪空格的换行 → 把段落画进缓冲区。

调用关系:它实现 Renderable 接口,被终端 UI 渲染流程调用;文本来源仍是 HookCell::display_lines。

调用图:调用 1 个内部函数(display_lines);外部调用 2 个(new, from)。

HookCell::desired_height373–375 ↗
fn desired_height(&self, width: u16) -> u16

作用:告诉布局系统这个 HookCell 大概需要多高。这样外层可以给它分配合适的屏幕空间。

数据流:输入宽度 → 调用 HistoryCell 的默认高度计算 → 返回需要的行数。

调用关系:它实现 Renderable 接口;把计算委托给 HistoryCell::desired_height,保持与历史单元通用规则一致。

调用图:调用 1 个内部函数(desired_height)。

HookRunCell::expire_quiet_linger_now_for_test380–387 ↗
fn expire_quiet_linger_now_for_test(&mut self)

作用:测试专用:如果这个 run 正处在安静成功后的短暂停留,就把它的移除时间改成现在。

数据流:检查当前 state → 只有 QuietLinger 状态会被修改 → 把 removal_deadline 写成当前时间 → 没有返回值。

调用关系:它被 HookCell::expire_quiet_runs_now_for_test 调用,用来快速制造“停留已过期”的测试场景。

调用图:外部调用 1 个(now)。

HookRunCell::reveal_running_now_for_test390–397 ↗
fn reveal_running_now_for_test(&mut self, now: Instant)

作用:测试专用:如果这个 run 还在等待显示,就让它现在到达显示时间。

数据流:输入 now → 检查 state 是否为 PendingReveal → 是的话把 reveal_deadline 改成 now → 没有返回值。

调用关系:它被 HookCell::reveal_running_runs_now_for_test 调用,帮助测试跳过 300 毫秒等待。

HookRunCell::reveal_running_after_delayed_redraw_for_test400–410 ↗
fn reveal_running_after_delayed_redraw_for_test(&mut self, now: Instant)

作用:测试专用:把显示截止时间调到过去,模拟界面晚了一会儿才刷新。

数据流:输入 now → 如果状态是 PendingReveal,就计算一个比现在更早的截止时间 → 写回 reveal_deadline → 没有返回值。

调用关系:它被 HookCell::reveal_running_runs_after_delayed_redraw_for_test 调用,用来覆盖延迟重绘相关的边界情况。

调用图:外部调用 2 个(from_millis, checked_sub)。

HookRunCell::running_group_key413–420 ↗
fn running_group_key(&self) -> Option<RunningHookGroupKey>

作用:判断这个 run 是否能参与“运行中 hook 合并显示”,并给出分组依据。只有正在可见运行的 hook 才能分组。

数据流:读取 run 的状态、事件名和状态消息 → 如果状态可视为运行中,就返回由事件名和状态消息组成的 key → 否则返回空。

调用关系:它被 HookCell::display_lines 使用;内部用 HookRunState::is_running_visible 先确认这个 run 是否适合合并。

调用图:调用 1 个内部函数(is_running_visible)。

HookRunCell::push_display_lines423–465 ↗
fn push_display_lines(&self, lines: &mut Vec<Line<'static>>, animations_enabled: bool)

作用:把单个、不合并的 hook run 写成显示行。已完成的 hook 会带状态和输出内容,运行中的 hook 会显示运行标题。

数据流:输入可追加的 lines 列表和动画开关 → 根据状态选择输出方式 → 运行中写标题,已完成写项目符号、状态和多行输出 → 修改 lines,不返回新值。

调用关系:它被 HookCell::display_lines 在遇到不能分组的完成项时调用;会用 hook_event_label、hook_completed_bullet、hook_output_prefix 和 push_running_hook_header。

调用图:调用 4 个内部函数(hook_completed_bullet, hook_event_label, hook_output_prefix, push_running_hook_header);外部调用 2 个(format!, vec!)。

HookRunState::pending470–475 ↗
fn pending(start_time: Instant) -> Self

作用:创建一个 hook 刚开始时的隐藏状态。这样非常快的 hook 不会在屏幕上一闪而过。

数据流:输入开始时间 → 计算 300 毫秒后的 reveal_deadline → 返回 PendingReveal 状态,里面记住开始时间和允许显示的时间。

调用关系:它被 HookCell::start_run 调用,是所有实时 hook 的初始状态。

调用图:被 1 处调用(start_run)。

HookRunState::completed478–480 ↗
fn completed(status: HookRunStatus, entries: Vec<HookOutputEntry>) -> Self

作用:创建一个已完成且需要保留的状态。它保存最终状态和输出内容,供历史显示。

数据流:输入 HookRunStatus 和输出 entries → 包装成 Completed 状态 → 返回这个状态。

调用关系:它被 HookCell::complete_run 和 HookCell::add_completed_run 调用,用于保存失败、警告、有输出等值得留下的结果。

调用图:被 2 处调用(add_completed_run, complete_run)。

HookRunState::is_active483–490 ↗
fn is_active(&self) -> bool

作用:判断这个状态是否还可能继续变化。等待显示、正在运行、短暂停留都算活跃;已完成保留则不算。

数据流:读取枚举状态 → 按状态种类匹配 → 返回 true 或 false,不改动状态。

调用关系:它被 HookCell::is_active 间接用于判断整个 HookCell 是否还在活动。

HookRunState::should_render493–500 ↗
fn should_render(&self) -> bool

作用:判断这个状态现在是否应该占一行显示。隐藏等待期不显示,其他可见或完成状态显示。

数据流:读取状态 → PendingReveal 返回 false,VisibleRunning、QuietLinger、Completed 返回 true → 不修改任何内容。

调用关系:它被 HookCell::should_render 和 HookCell::display_lines 使用,决定是否跳过某个 run。

HookRunState::has_persistent_output503–512 ↗
fn has_persistent_output(&self) -> bool

作用:判断完成后的 hook 是否应该长期留在历史里。只有非成功状态,或者有输出内容的完成状态,才算值得保留。

数据流:读取状态 → 如果是 Completed,就检查状态是否不是 Completed 或 entries 是否非空 → 其他状态直接返回 false。

调用关系:它被 HookCell::take_completed_persistent_runs 使用,用来分开“要进入历史”和“可以消失”的 run。

HookRunState::start_time517–524 ↗
fn start_time(&self) -> Option<Instant>

作用:取出活跃状态最初开始的时间。这个时间主要用于动画节奏和合并显示时保持稳定。

数据流:读取状态 → PendingReveal、VisibleRunning、QuietLinger 返回保存的 start_time → Completed 返回空,因为完成后不再动画。

调用关系:它被显示分组、动画节拍等流程使用,保证 spinner 或闪动文字不会因为状态切换而突然重置。

HookRunState::is_running_visible527–532 ↗
fn is_running_visible(&self) -> bool

作用:判断这个 run 是否应该当作“正在运行的一行”来处理。短暂停留的安静成功也算,因为用户看到的仍是运行样式。

数据流:读取状态 → 如果是 VisibleRunning 或 QuietLinger 返回 true → 其他返回 false。

调用关系:它被 HookRunCell::running_group_key、HookCell::has_visible_running_run 和动画节拍逻辑使用。

调用图:被 1 处调用(running_group_key);外部调用 1 个(matches!)。

HookRunState::reveal_if_due538–554 ↗
fn reveal_if_due(&mut self, now: Instant) -> bool

作用:到时间后把隐藏中的 hook 变成可见的运行中 hook。没到时间或不是隐藏状态时什么也不做。

数据流:输入当前时间 now → 如果状态是 PendingReveal 且 now 已到 reveal_deadline → 改成 VisibleRunning 并记录 visible_since → 返回是否发生了变化。

调用关系:它被 HookCell::advance_time 调用,是定时显示策略的关键一步。

HookRunState::next_timer_deadline557–567 ↗
fn next_timer_deadline(&self) -> Option<Instant>

作用:告诉外层这个状态下一次需要关注的时间。隐藏状态关注显示时间,短暂停留关注移除时间。

数据流:读取状态 → PendingReveal 返回 reveal_deadline,QuietLinger 返回 removal_deadline,其他状态返回空。

调用关系:它被 HookCell::next_timer_deadline 汇总,帮助外层安排下一次定时刷新。

HookRunState::quiet_linger_expired570–579 ↗
fn quiet_linger_expired(&self, now: Instant) -> bool

作用:判断安静成功后的短暂停留是否已经结束。结束后这个 run 就可以被删除。

数据流:输入当前时间 now → 如果是 QuietLinger,就比较 now 是否到达 removal_deadline → 返回是否过期;其他状态返回 false。

调用关系:它被 HookCell::advance_time 用在 retain 过滤中,用来清走到期的安静成功记录。

HookRunState::complete_quiet_success585–604 ↗
fn complete_quiet_success(&mut self, now: Instant) -> bool

作用:处理“可见后安静成功”的收尾。若用户刚看到它,就让它继续停留到最短可见时间;否则告诉调用者可以直接删掉。

数据流:输入当前时间 now → 只有 VisibleRunning 状态会处理 → 计算 visible_since 加 600 毫秒的最早移除时间 → 如果还没到,就改成 QuietLinger 并返回 true;如果已到或不适用,返回 false。

调用关系:它被 HookCell::complete_run 在安静成功时调用,是避免文字刚出现就消失的关键规则。

RunningHookGroup::new608–614 ↗
fn new(key: RunningHookGroupKey, start_time: Option<Instant>) -> Self

作用:创建一个运行中 hook 的显示分组。分组用于把相邻且同类的多个 hook 合成一行。

数据流:输入分组 key 和开始时间 → 设置 count 为 1 → 返回新的 RunningHookGroup。

调用关系:它被 HookCell::display_lines 在遇到新的运行中分组时调用,后续同组 run 会增加 count。

调用图:被 1 处调用(display_lines)。

push_running_hook_group618–637 ↗
fn push_running_hook_group(
    lines: &mut Vec<Line<'static>>,
    group: &RunningHookGroup,
    animations_enabled: bool,
)

作用:把一个运行中 hook 分组输出成一行文字。一个 hook 显示单数,多个 hook 显示数量。

数据流:输入 lines、分组和动画开关 → 先按需加空行分隔 → 根据事件名和数量拼出标题 → 调用统一的运行标题输出函数 → 修改 lines。

调用关系:它被 HookCell::display_lines 在分组结束或最后收尾时调用;具体标题样式交给 push_running_hook_header。

调用图:调用 3 个内部函数(hook_event_label, push_hook_line_separator, push_running_hook_header);被 1 处调用(display_lines);外部调用 1 个(format!)。

push_running_hook_header640–666 ↗
fn push_running_hook_header(
    lines: &mut Vec<Line<'static>>,
    hook_text: &str,
    start_time: Option<Instant>,
    status_message: Option<&str>,
    animations_enabled: bool,
)

作用:生成所有“正在运行 hook”共用的标题行。它负责 spinner、闪动文字、粗体 fallback 和状态说明。

数据流:输入 lines、标题文字、开始时间、可选状态消息和动画开关 → 根据动画设置选择运动模式 → 可能加入活动指示符,再加入标题文字和状态消息 → 追加一行到 lines。

调用关系:它被 HookRunCell::push_display_lines 和 push_running_hook_group 共用,确保单个和合并的运行中 hook 样式一致。

调用图:调用 3 个内部函数(from_animations_enabled, activity_indicator, shimmer_text);被 2 处调用(push_display_lines, push_running_hook_group);外部调用 2 个(default, new)。

push_hook_line_separator669–673 ↗
fn push_hook_line_separator(lines: &mut Vec<Line<'static>>)

作用:在两个 hook 块之间加一个空行,但不会在最开头加空行。这样显示更清楚又不浪费第一行。

数据流:输入 lines → 如果当前已有内容,就追加一个空行 → 如果还没有内容就不做事。

调用关系:它被 HookCell::display_lines 和 push_running_hook_group 调用,用来控制 hook 块之间的间距。

调用图:被 2 处调用(display_lines, push_running_hook_group)。

earliest_instant676–683 ↗
fn earliest_instant(left: Option<Instant>, right: Option<Instant>) -> Option<Instant>

作用:从两个可选时间里选出最早的一个。它用于合并多个运行中 hook 时保持动画按最早开始的那个算。

数据流:输入两个 Option<Instant> → 两个都有就取较早的;只有一个就取那个;都没有就返回空。

调用关系:它被 HookCell::display_lines 用在分组合并时,避免后来加入的 hook 让 spinner 节奏重置。

调用图:被 1 处调用(display_lines)。

new_active_hook_cell685–687 ↗
fn new_active_hook_cell(run: HookRunSummary, animations_enabled: bool) -> HookCell

作用:对外提供一个创建活动 hook 单元的包装函数。外部代码不用直接碰 HookCell::new_active。

数据流:输入 hook 摘要和动画设置 → 调用 HookCell::new_active → 返回新建的 HookCell。

调用关系:它是本文件给其他模块使用的公开入口之一,把创建活动单元的细节藏在 HookCell 内部。

调用图:调用 1 个内部函数(new_active)。

new_completed_hook_cell689–691 ↗
fn new_completed_hook_cell(run: HookRunSummary, animations_enabled: bool) -> HookCell

作用:对外提供一个创建已完成 hook 历史单元的包装函数。它用于历史恢复或已有完成结果的展示。

数据流:输入 hook 摘要和动画设置 → 调用 HookCell::new_completed → 返回新建的 HookCell。

调用关系:它是本文件给其他模块使用的公开入口之一,内部会自动跳过安静成功的 hook。

调用图:调用 1 个内部函数(new_completed)。

hook_run_is_quiet_success694–696 ↗
fn hook_run_is_quiet_success(run: &HookRunSummary) -> bool

作用:判断一次 hook 是否是“不值得留下痕迹的成功”。也就是状态成功,并且没有任何输出。

数据流:读取 HookRunSummary 的 status 和 entries → 如果 status 是 Completed 且 entries 为空,返回 true → 否则返回 false。

调用关系:它被 HookCell::complete_run 和 HookCell::add_completed_run 调用,是本文件“安静成功不打扰用户”规则的核心判断。

调用图:被 2 处调用(add_completed_run, complete_run)。

hook_completed_bullet698–713 ↗
fn hook_completed_bullet(status: HookRunStatus, entries: &[HookOutputEntry]) -> Span<'static>

作用:为已完成的 hook 选择前面的圆点样式。颜色和粗细帮助用户快速看出结果是正常、警告还是失败。

数据流:输入完成状态和输出 entries → 成功无警告用绿色粗圆点,成功但有警告用普通粗圆点,失败/阻止/停止用红色粗圆点,运行中用普通圆点 → 返回 Span 样式文本。

调用关系:它被 HookRunCell::push_display_lines 用于完成项标题,也被测试 tests::completed_hook_with_warning_uses_default_bold_bullet 单独验证。

调用图:被 2 处调用(push_display_lines, completed_hook_with_warning_uses_default_bold_bullet);外部调用 1 个(iter)。

hook_output_prefix715–723 ↗
fn hook_output_prefix(kind: HookOutputEntryKind) -> &'static str

作用:把 hook 输出类型转换成用户能看懂的文字前缀。比如 warning 会显示成“warning: ”。

数据流:输入 HookOutputEntryKind → 按类型匹配成固定字符串前缀 → 返回这个前缀。

调用关系:它被 HookRunCell::push_display_lines 用在每条输出的第一行前面,让不同输出含义一眼可分。

调用图:被 1 处调用(push_display_lines)。

hook_event_label725–738 ↗
fn hook_event_label(event_name: HookEventName) -> &'static str

作用:把 hook 事件枚举转换成显示用的名字。枚举是程序内部值,标签是给用户看的文字。

数据流:输入 HookEventName → 按事件类型匹配对应的固定英文标签 → 返回静态字符串。

调用关系:它被 HookRunCell::push_display_lines 和 push_running_hook_group 使用,出现在“Running ... hook”和完成标题中。

调用图:被 2 处调用(push_display_lines, push_running_hook_group)。

tests::completed_hook_with_warning_uses_default_bold_bullet749–760 ↗
fn completed_hook_with_warning_uses_default_bold_bullet()

作用:测试:成功但带 warning 的 hook 不应该显示绿色圆点,而应该是默认颜色的粗圆点。

数据流:构造一条 warning 输出 → 调用 hook_completed_bullet → 检查圆点内容、颜色和粗体样式是否符合预期。

调用关系:它直接验证 hook_completed_bullet 的一个重要视觉规则,防止警告被误画成完全正常的绿色成功。

调用图:调用 1 个内部函数(hook_completed_bullet);外部调用 3 个(assert!, assert_eq!, vec!)。

tests::completed_hook_multiline_context_preserves_display_and_raw_lines763–783 ↗
fn completed_hook_multiline_context_preserves_display_and_raw_lines()

作用:测试:多行 context 输出在屏幕显示和纯文本输出中都应该保持正确缩进和空行。

数据流:构造一个带多行 context 的已完成 HookCell → 分别生成 display_lines 和 raw_lines → 与预期文本列表比较。

调用关系:它通过 tests::completed_hook_cell 创建样例,覆盖 HookRunCell::push_display_lines 和 HookCell::raw_lines 的配合。

调用图:外部调用 3 个(assert_eq!, completed_hook_cell, vec!)。

tests::completed_hook_multiline_warning_prefixes_first_line_only786–804 ↗
fn completed_hook_multiline_warning_prefixes_first_line_only()

作用:测试:多行 warning 输出只在第一行加“warning: ”前缀,后续行只缩进。

数据流:构造一个带两行 warning 的已完成 HookCell → 生成显示文本 → 检查第一行有前缀、第二行只有正文缩进。

调用关系:它验证 HookRunCell::push_display_lines 对多行输出的格式规则,避免每行重复前缀造成难读。

调用图:外部调用 3 个(assert_eq!, completed_hook_cell, vec!)。

tests::pending_hook_does_not_animate_transcript807–812 ↗
fn pending_hook_does_not_animate_transcript()

作用:测试:还在隐藏等待期的 hook 不应该让 transcript 动画刷新。因为用户还看不到它。

数据流:创建一个活动 HookCell → 不推进到可见状态 → 调用 transcript_animation_tick → 断言返回空。

调用关系:它使用 HookCell::new_active 和测试摘要 tests::hook_run_summary,验证隐藏期不会触发动画节拍。

调用图:调用 1 个内部函数(new_active);外部调用 2 个(assert_eq!, hook_run_summary)。

tests::visible_hook_animates_transcript_when_animations_enabled815–822 ↗
fn visible_hook_animates_transcript_when_animations_enabled()

作用:测试:可见的运行中 hook 在开启动画时应该提供 transcript 动画节拍。

数据流:创建活动 HookCell → 强制 reveal → 推进时间 → 调用 transcript_animation_tick → 检查返回 Some(0)。

调用关系:它验证 HookCell::transcript_animation_tick 与测试 reveal 方法、advance_time 的配合。

调用图:调用 1 个内部函数(new_active);外部调用 3 个(now, assert_eq!, hook_run_summary)。

tests::visible_hook_does_not_animate_transcript_when_animations_disabled825–834 ↗
fn visible_hook_does_not_animate_transcript_when_animations_disabled()

作用:测试:即使 hook 已经可见,只要关闭动画,就不应该产生 transcript 动画节拍。

数据流:用 animations_enabled=false 创建活动 HookCell → 强制显示并推进时间 → 查询 transcript_animation_tick → 断言返回空。

调用关系:它覆盖动画开关的行为,确保 HookCell::transcript_animation_tick 尊重全局动画设置。

调用图:调用 1 个内部函数(new_active);外部调用 3 个(now, assert_eq!, hook_run_summary)。

tests::visible_hook_without_animations_omits_spinner837–855 ↗
fn visible_hook_without_animations_omits_spinner()

作用:测试:关闭动画时,运行中的 hook 标题不应该带 spinner,只显示静态文字和状态消息。

数据流:创建关闭动画的活动 HookCell → 强制显示并推进时间 → 生成 display_lines → 把行转成纯文本 → 与预期的一行静态文本比较。

调用关系:它验证 push_running_hook_header 在无动画模式下的输出,也间接覆盖 HookCell::display_lines。

调用图:调用 1 个内部函数(new_active);外部调用 3 个(now, assert_eq!, hook_run_summary)。

tests::completed_hook_cell857–870 ↗
fn completed_hook_cell(
        event_name: HookEventName,
        status: HookRunStatus,
        entries: Vec<HookOutputEntry>,
    ) -> HookCell

作用:测试辅助函数:快速造一个已完成 hook 的 HookCell,省去每个测试重复填写字段。

数据流:输入事件名、状态和输出 entries → 从 tests::hook_run_summary 取默认摘要 → 改成已完成状态并填入输出 → 调用 HookCell::new_completed 返回 HookCell。

调用关系:它被多个完成态显示测试调用,是测试数据工厂,不参与正式运行。

调用图:调用 1 个内部函数(new_completed);外部调用 1 个(hook_run_summary)。

tests::line_texts872–874 ↗
fn line_texts(lines: &[Line<'_>]) -> Vec<String>

作用:测试辅助函数:把多行带样式的 Line 转成普通字符串列表。这样断言更直观。

数据流:输入 Line 切片 → 对每一行调用 tests::line_text → 收集成 Vec<String> 返回。

调用关系:它被多行显示测试使用;真正拼接单行文本的工作交给 tests::line_text。

调用图:外部调用 1 个(iter)。

tests::line_text876–881 ↗
fn line_text(line: &Line<'_>) -> String

作用:测试辅助函数:把一行里多个带样式片段拼成普通字符串。它忽略颜色、粗体等样式,只看文字内容。

数据流:输入一个 Line → 遍历其中所有 span → 取出每段 content 并拼接 → 返回完整字符串。

调用关系:它被 tests::line_texts 和部分测试中的 map 调用,让测试关注文本结果而不是终端样式对象。

tests::hook_run_summary883–900 ↗
fn hook_run_summary(id: &str) -> HookRunSummary

作用:测试辅助函数:生成一份默认的 hook 运行摘要。测试可以在此基础上改少数字段。

数据流:输入 id → 填充事件名、处理类型、路径、状态消息、运行状态等默认字段 → 返回 HookRunSummary。

调用关系:它被多个测试和 tests::completed_hook_cell 使用,提供稳定的基础测试数据。

调用图:外部调用 2 个(new, test_path_buf)。

聊天组件流式处理与轮次状态

这些文件将底层流和工具事件转换为实时聊天组件状态、转录更新、命令和 hook 生命周期变化,以及最终确定的轮次行为。

tui/src/chatwidget/exec_state.rs源码 ↗
domain_logic命令执行和终端交互期间

聊天界面不只是显示文字,它还会展示系统正在执行的命令,比如启动某个进程、运行工具、等待用户交互。这个文件就像一张小账本,专门记录这些命令的状态。比如 RunningCommand 记住原始命令、解析后的命令含义,以及命令来源;UnifiedExecProcessSummary 保存一个进程的摘要,方便界面展示最近输出;UnifiedExecWaitState 和 UnifiedExecWaitStreak 用来避免同一个等待提示反复刷屏。这里还提供几个判断和转换函数:判断命令是否属于“统一执行”来源,把一整串命令拆成参数列表,把协议层传来的命令动作转成核心内部能理解的 ParsedCommand。它本身不真正执行命令,而是帮 ChatWidget 在命令执行前后把信息整理清楚。

函数细节7
UnifiedExecWaitState::new26–28 ↗
fn new(command_display: String) -> Self

作用:创建一个新的等待状态,记住当前正在等待显示的命令文字。这样后面可以判断是不是又来了同一条等待提示。

数据流:进去的是一段 command_display,也就是准备显示给用户看的命令文字 → 函数把它存进 UnifiedExecWaitState 这个小状态对象里 → 出来的是一个新的等待状态对象,没有改动别的东西。

调用关系:当 handle_command_execution_started_now 发现命令刚开始执行时,会用它建立等待状态。后续界面如果又收到类似提示,就可以拿这个状态来比对,避免重复显示。

调用图:被 1 处调用(handle_command_execution_started_now)。

UnifiedExecWaitState::is_duplicate30–32 ↗
fn is_duplicate(&self, command_display: &str) -> bool

作用:判断新来的命令显示文字,和已经记录的等待命令是不是同一条。它的作用是防止界面把同一个等待信息重复打印出来。

数据流:进去的是一段新的 command_display 字符串,同时函数读取自己之前保存的 command_display → 它做一次简单比较,看两段文字是否完全一样 → 出来的是 true 或 false,表示是不是重复,没有修改状态。

调用关系:它通常会在已有等待状态之后被调用,用来帮聊天界面决定:这条等待提示要不要继续显示,还是应该当作重复消息跳过。

UnifiedExecWaitStreak::new42–47 ↗
fn new(process_id: String, command_display: Option<String>) -> Self

作用:创建一段连续等待记录,把某个进程编号和可选的命令显示文字绑在一起。它会自动丢掉空字符串,避免把“什么都没写”的命令当成有效内容。

数据流:进去的是 process_id,也就是进程的身份标记,还有一个可能存在的 command_display → 函数保存进程编号,并检查命令文字:如果是空的就当作没有 → 出来的是一个新的 UnifiedExecWaitStreak 对象。

调用关系:on_terminal_interaction 在处理终端交互时会调用它,用来记录某个进程进入了连续等待状态。之后如果命令文字晚一点才出现,还可以交给 update_command_display 补上。

调用图:被 1 处调用(on_terminal_interaction)。

UnifiedExecWaitStreak::update_command_display49–54 ↗
fn update_command_display(&mut self, command_display: Option<String>)

作用:给一段等待记录补充命令显示文字,但只在原来还没有文字时才补。这样可以避免后来来的信息覆盖掉已经确定的显示内容。

数据流:进去的是一个可能存在的 command_display,同时函数查看自己当前是否已经有 command_display → 如果已经有了,就什么也不做;如果没有,就保存这次传入的非空文字 → 结果是这个等待记录可能被补上命令文字,也可能保持不变。

调用关系:它服务于等待记录的后续更新。当统一执行流程先知道进程、后知道命令内容时,这个函数负责把缺失的信息补齐,同时保护已有信息不被乱改。

is_unified_exec_source57–62 ↗
fn is_unified_exec_source(source: ExecCommandSource) -> bool

作用:判断一条命令是不是来自“统一执行”流程。统一执行可以理解成界面把启动命令和交互命令放到同一套流程里处理,方便统一展示和追踪。

数据流:进去的是 source,也就是命令来源 → 函数检查它是不是 UnifiedExecStartup 或 UnifiedExecInteraction 这两种来源之一 → 出来的是 true 或 false,表示是否属于统一执行来源。

调用关系:它是给其他命令处理代码用的分流判断。调用者先用它看命令来源,如果属于统一执行,就走统一执行相关的等待、摘要和交互展示逻辑。函数内部只是做枚举匹配,不再把工作交给复杂模块。

调用图:外部调用 1 个(matches!)。

is_standard_tool_call64–69 ↗
fn is_standard_tool_call(parsed_cmd: &[ParsedCommand]) -> bool

作用:判断解析后的命令是不是一组正常可识别的工具调用。只要里面有 Unknown,也就是系统没认出来的命令片段,就不算标准工具调用。

数据流:进去的是 parsed_cmd,一组已经解析好的命令动作 → 函数先确认这组内容不是空的,再逐个检查每一项是不是都不是 Unknown → 出来的是 true 或 false,表示这批命令能不能当作标准工具调用来处理。

调用关系:它通常用于命令展示或执行状态判断之前,帮上层代码决定这条命令能不能按“标准工具”方式展示。它只读取 ParsedCommand 列表,不修改任何数据。

调用图:外部调用 2 个(is_empty, iter)。

command_execution_command_and_parsed71–83 ↗
fn command_execution_command_and_parsed(
    command: &str,
    command_actions: &[codex_app_server_protocol::CommandAction],
) -> (Vec<String>, Vec<ParsedCommand>)

作用:把协议层传来的命令信息整理成界面和核心逻辑都好用的两份数据:一份是拆好的命令参数,一份是解析后的命令动作。

数据流:进去的是 command 字符串,以及 command_actions 这组协议层命令动作 → 函数先用 split_command_string 把整条命令拆成类似 argv 的字符串列表,再把每个 CommandAction 转成核心内部的 ParsedCommand → 出来的是一个二元结果:Vec<String> 命令参数列表和 Vec<ParsedCommand> 解析结果列表。

调用关系:它位于协议数据进入聊天界面命令状态之前的转换环节。上层拿到服务器协议里的命令执行信息后,会用这个函数把外部格式转成内部格式;它把拆命令的细活交给 split_command_string,把动作转换交给 CommandAction::into_core。

调用图:调用 1 个内部函数(split_command_string);外部调用 1 个(iter)。

tui/src/chatwidget/status_state.rs源码 ↗
domain_logicmain loop / status updates

聊天界面不是只显示对话内容,还要告诉用户“系统现在在干什么”。这个文件就像一块小黑板,专门记录这些提示文字。它保存当前状态栏的标题、详细说明、最多显示几行,还单独处理一种重要情况:有多个审批请求正在等待人工确认。多个审批同时发生时,它会把它们合并成一条好懂的状态,比如“正在审核 2 个审批请求”,并列出前几个详情,避免界面乱跳。它还记住终端标题该显示“Working”“Thinking”这类短标签,因为窗口标题不能太长。另一个细节是重试时的状态恢复:它会先记住重试前的标题,之后只能取回一次,防止旧状态反复覆盖新状态。

函数细节12
StatusIndicatorState::working13–19 ↗
fn working() -> Self

作用:创建一个默认的“正在工作”状态。别人需要把状态栏恢复到最普通、最安全的工作提示时,会用它。

数据流:进去不需要任何输入 → 它固定填入标题“Working”,没有详情,并使用默认的详情行数限制 → 出来一个完整的 StatusIndicatorState,可直接放到界面状态栏里。

调用关系:它是状态系统的起点之一。StatusState::default 会调用它,让新建的聊天界面一开始就有一个明确的状态,而不是空白或未定义。

调用图:被 1 处调用(default);外部调用 1 个(from)。

StatusIndicatorState::is_guardian_review21–23 ↗
fn is_guardian_review(&self) -> bool

作用:判断当前状态是不是“审批审核中”。这让外层代码能知道当前状态栏是否正在显示审批相关内容。

数据流:进去的是一个已有的状态栏状态 → 它查看 header 文字是否等于“Reviewing approval request”或以“Reviewing ”开头 → 出来一个 true/false,表示这是不是审批审核状态。

调用关系:它是一个小判断工具,供状态切换时识别特殊的审批状态。它不改任何数据,只回答“现在是不是这一类状态”。

PendingGuardianReviewStatus::start_or_update51–58 ↗
fn start_or_update(&mut self, id: String, detail: String)

作用:登记一个正在等待审批的请求,或者更新同一个请求的说明文字。这样同一个审批不会重复出现,只会刷新内容。

数据流:进去一个审批 id 和一段详情文字 → 它先在现有列表里找同样的 id;找到了就改详情,没找到就新增一条 → 出来没有返回值,但内部的待审批列表已经变成最新状态。

调用关系:当新的审批请求开始,或已有请求的信息变化时,聊天界面会走到这里。之后 PendingGuardianReviewStatus::status_indicator_state 可以把这些条目汇总成用户能看到的状态栏文字。

PendingGuardianReviewStatus::finish60–64 ↗
fn finish(&mut self, id: &str) -> bool

作用:标记某个审批请求已经结束,并把它从待审批列表里移走。它还会告诉调用者,这次到底有没有真的移除东西。

数据流:进去一个审批 id → 它记录原来的条目数量,然后删除所有 id 匹配的条目 → 出来一个 true/false;true 表示确实删掉了某个待审批请求,false 表示原本就没有这个请求。

调用关系:审批完成或取消时会用它清理状态。清理后,外层通常会再决定状态栏要继续显示剩下的审批,还是恢复成普通状态。

PendingGuardianReviewStatus::is_empty66–68 ↗
fn is_empty(&self) -> bool

作用:检查当前是不是没有任何待审批请求。这个结果常用来判断状态栏是否还需要显示审批提示。

数据流:进去的是当前待审批状态对象 → 它只看内部列表是不是空的 → 出来一个 true/false,不修改任何内容。

调用关系:它像一个简单的“还有没有排队的人”的检查。审批流程结束后,调用方可以用它决定是否退出审批显示模式。

PendingGuardianReviewStatus::status_indicator_state74–104 ↗
fn status_indicator_state(&self) -> Option<StatusIndicatorState>

作用:把所有正在等待的审批请求,整理成一条可以显示在状态栏里的文字。它负责把零散的审批条目变成用户看得懂的汇总提示。

数据流:进去的是当前保存的待审批列表 → 如果只有一条,就显示这一条详情;如果有多条,就最多列出前三条,并在还有更多时加上“+N more”;如果没有条目,就没有状态可显示 → 出来一个可选的 StatusIndicatorState;有待审批时返回状态栏内容,没有时返回 None。

调用关系:这是审批状态显示的核心拼装步骤。PendingGuardianReviewStatus::start_or_update 和 PendingGuardianReviewStatus::finish 改变列表后,外层代码会依靠它生成新的状态栏快照;它内部会使用格式化文字来生成项目符号和数量提示。

调用图:外部调用 2 个(from, format!)。

StatusState::default117–125 ↗
fn default() -> Self

作用:创建聊天界面状态系统的初始状态。它保证界面一启动,就有合理的状态栏、空的审批列表,以及默认的终端标题状态。

数据流:进去不需要输入 → 它调用 StatusIndicatorState::working 生成“Working”状态,创建空的待审批记录,把终端标题状态设为 Working,并清空重试相关缓存 → 出来一个完整可用的 StatusState。

调用关系:它会在聊天组件创建时使用,例如 new_with_op_target 会需要这份初始状态。测试 retry_status_header_is_taken_once 也用它来验证重试标题缓存的行为。

调用图:调用 1 个内部函数(working);被 2 处调用(new_with_op_target, retry_status_header_is_taken_once);外部调用 1 个(default)。

StatusState::set_status129–131 ↗
fn set_status(&mut self, status: StatusIndicatorState)

作用:把当前状态栏替换成新的状态。外层代码想让界面显示“正在思考”“正在等待”等新提示时,会用它。

数据流:进去一个新的 StatusIndicatorState → 它直接覆盖 current_status → 出来没有返回值,但状态栏缓存已经指向新的显示内容。

调用关系:它是外部更新状态栏的简单入口。别的流程先构造好要显示的状态,再交给它保存,之后渲染界面时就能读到新状态。

StatusState::take_retry_status_header133–135 ↗
fn take_retry_status_header(&mut self) -> Option<String>

作用:取出之前为“重试”临时记住的状态标题,并且取完就清掉。这样旧标题只会被恢复一次,不会反复影响后面的状态。

数据流:进去的是当前 StatusState → 它拿走 retry_status_header 里的文字,同时把这个位置清空 → 出来一个可选的字符串;有缓存就返回标题,没有缓存就返回 None。

调用关系:restore_retry_status_header_if_present 会调用它来尝试恢复重试前的标题。因为它是“取走”而不是“查看”,所以能避免同一个旧标题被重复恢复。

调用图:被 1 处调用(restore_retry_status_header_if_present)。

StatusState::remember_retry_status_header137–141 ↗
fn remember_retry_status_header(&mut self)

作用:在进入重试流程前,记住当前状态栏标题。它只在还没记过的时候保存,避免把最早的原始状态覆盖掉。

数据流:进去的是当前 StatusState → 它检查 retry_status_header 是否为空;如果为空,就复制 current_status.header 存进去;如果已经有值,就什么都不做 → 出来没有返回值,但可能多了一份待恢复的标题缓存。

调用关系:它通常在重试开始时使用,和 StatusState::take_retry_status_header 配成一对:一个先保存,一个之后恢复并清除。

tests::guardian_status_aggregates_parallel_reviews151–164 ↗
fn guardian_status_aggregates_parallel_reviews()

作用:验证多个审批请求同时存在时,状态栏会显示成一条汇总信息,而不是丢失或只显示其中一个。

数据流:进去是测试里新建的空待审批状态 → 它加入两个审批条目,再生成状态栏状态 → 出来通过断言确认标题、详情文本和最大行数都符合预期。

调用关系:这是对 PendingGuardianReviewStatus::start_or_update 和 PendingGuardianReviewStatus::status_indicator_state 的行为检查。它用测试断言保证并行审批的显示格式不会被后续改坏。

调用图:外部调用 2 个(assert_eq!, default)。

tests::retry_status_header_is_taken_once167–178 ↗
fn retry_status_header_is_taken_once()

作用:验证重试前保存的状态标题只能取回一次。这样可以防止旧状态在界面上重复回滚。

数据流:进去是一个默认 StatusState,测试把当前标题改成“Thinking” → 它调用 remember_retry_status_header 保存,再连续调用 take_retry_status_header 两次 → 第一次得到“Thinking”,第二次得到 None,说明缓存已经清空。

调用关系:这个测试覆盖 StatusState::default、StatusState::remember_retry_status_header 和 StatusState::take_retry_status_header 的配合方式,确保重试状态恢复机制按“一次性票据”的方式工作。

调用图:调用 1 个内部函数(default);外部调用 1 个(assert_eq!)。

tui/src/chatwidget/transcript.rs源码 ↗
domain_logicmain loop / 聊天内容更新、回滚、复制和渲染刷新时

聊天窗口不是只把文字画出来就完了。用户可能要复制上一条助手回答,系统可能回滚本地线程,助手还可能一边流式输出一边更新计划。这个文件里的 TranscriptState 就像聊天界面的记事本:它记录当前正在活动的显示单元、用户已经显示了几轮、哪些助手回答还能被复制、最近的计划内容,以及本轮是否做过工具调用或计划更新。它还用 active_cell_revision 这个递增编号给缓存“盖时间戳”,内容变了就让旧缓存失效。为了不无限占内存,可复制的助手回复只保留有限份;回滚时也会把已经不该存在的回复删掉。

函数细节9
TranscriptState::new50–55 ↗
fn new(active_cell: Option<Box<dyn HistoryCell>>) -> Self

作用:创建一份新的聊天记录状态,可以顺手放入一个当前正在显示的消息格子。外部创建 ChatWidget 时会用它把“初始界面状态”准备好。

数据流:进去的是一个可选的 active_cell,也就是当前活动的历史消息显示单元;函数先用默认值填好其他所有记录项,再把这个 active_cell 放进去;出来的是一份完整的 TranscriptState,新聊天界面可以直接拿来用。

调用关系:它被 new_with_op_target 调用,处在聊天界面初始化流程里。它自己把大部分字段交给默认值生成,只专门接收外面给的活动消息单元。

调用图:被 1 处调用(new_with_op_target);外部调用 1 个(default)。

TranscriptState::bump_active_cell_revision57–61 ↗
fn bump_active_cell_revision(&mut self)

作用:把“当前活动消息格子”的版本号加一。这样界面缓存就知道内容变过了,不能继续用旧画面。

数据流:进去的是可修改的 TranscriptState;函数读取 active_cell_revision,把它加一并写回去;出来没有返回值,但状态里的版本号变新了。如果数字大到溢出,它会从 0 重新开始,避免程序因为溢出出错。

调用关系:它被外层的 bump_active_cell_revision 包装函数调用。通常是在当前显示单元变化后用,提醒渲染相关逻辑:这块内容已经不是旧版本了。

调用图:被 1 处调用(bump_active_cell_revision)。

TranscriptState::record_agent_markdown63–81 ↗
fn record_agent_markdown(&mut self, markdown: String)

作用:记录一条刚完成的助手回复原始 Markdown 文本。这样用户之后点“复制回答”时,拿到的是干净的原文,而不是屏幕上加工过的显示效果。

数据流:进去的是助手回复的 markdown 字符串和当前状态里的可见用户轮数;函数检查最近一条可复制记录是不是同一轮的,如果是就覆盖更新,不是就新增一条;如果历史太多,就删掉最老的一条;最后把 last_agent_markdown 也更新,并标记本轮已经有可复制来源。

调用关系:它被外层 record_agent_markdown 调用,通常发生在助手回答完成时。它和 record_visible_user_turn、truncate_copy_history_to_user_turn_count 配合,用“第几轮用户提问”来判断哪些助手回答还属于当前可见对话。

调用图:被 1 处调用(record_agent_markdown)。

TranscriptState::record_visible_user_turn83–85 ↗
fn record_visible_user_turn(&mut self)

作用:记录界面上又出现了一轮用户发言。这个数字之后会用来给助手回复归档,方便复制和回滚时对齐。

数据流:进去的是当前 TranscriptState;函数把 visible_user_turn_count 安全地加一;出来没有返回值,但状态里“已显示用户轮数”增加了。这里用的是饱和加法,意思是数字到最大值后不会溢出乱跳。

调用关系:它被 record_visible_user_turn_for_copy 调用,通常在新的用户消息进入可见聊天记录时发生。之后 record_agent_markdown 会用这个轮数给助手回复打标签。

调用图:被 1 处调用(record_visible_user_turn_for_copy)。

TranscriptState::reset_copy_history87–93 ↗
fn reset_copy_history(&mut self)

作用:清空所有和“复制助手回答”有关的记忆。适合在对话被彻底重置,或者旧历史已经不能再信任时使用。

数据流:进去的是当前状态;函数把最近助手回复清掉,把可复制回复列表清空,把可见用户轮数归零,并重置回滚淘汰标记和本轮复制来源标记;出来没有返回值,但复制相关状态回到一张白纸。

调用关系:调用图里没有显示它被哪个函数直接调用,但它显然是给更大的聊天状态重置流程准备的。它不依赖别的函数,专门负责把复制历史这一块恢复到初始状态。

TranscriptState::truncate_copy_history_to_user_turn_count95–107 ↗
fn truncate_copy_history_to_user_turn_count(&mut self, user_turn_count: usize)

作用:在对话回滚后,把复制历史裁剪到指定的用户轮数。这样用户不会复制到已经被回滚掉、界面上不再存在的助手回答。

数据流:进去的是目标 user_turn_count 和当前状态;函数先把可见用户轮数改成这个目标值,再只保留轮数不超过它的助手回复;随后把最近助手回复改成裁剪后最后一条。如果原来有历史但裁完一条都不剩,就标记为“回滚把可复制来源淘汰了”。

调用关系:它服务于本地线程回滚一类流程。它接在已有复制历史之后使用,和 record_agent_markdown 形成前后关系:前者负责记录,当前函数负责在历史被撤销时删掉不该保留的记录。

TranscriptState::reset_turn_flags109–117 ↗
fn reset_turn_flags(&mut self)

作用:开始新一轮对话前,清掉“上一轮发生过什么”的临时标记。这样上一轮的工具调用、计划更新、复制来源等状态不会误影响下一轮。

数据流:进去的是当前状态;函数把本轮复制来源、计划更新、计划条目、工作活动等布尔标记设回 false,清掉最新 proposed plan 文本,清空流式计划缓冲区,并关闭计划条目正在流式输出的状态;出来没有返回值,但本轮临时状态被刷新。

调用关系:调用图里没有显示直接调用者,但它通常属于一轮聊天开始或结束时的整理动作。它不处理长期历史,只清理“这一轮”的短期信号,让后续显示和标题更新从干净状态开始。

tests::active_cell_revision_wraps127–136 ↗
fn active_cell_revision_wraps()

作用:这是一个测试,确认版本号加到 u64 最大值后会回到 0,而不是让程序因为数字溢出出问题。u64 可以理解为一种很大的非负整数类型。

数据流:进去的是一个专门构造的 TranscriptState,其中 active_cell_revision 被设成最大值;测试调用 bump_active_cell_revision;出来用断言检查版本号确实变成 0。

调用关系:它只在测试时运行。它直接验证 TranscriptState::bump_active_cell_revision 的边界行为,保证缓存失效用的版本号在极端情况下也安全。

调用图:外部调用 2 个(assert_eq!, default)。

tests::copy_history_tracks_latest_visible_turn139–150 ↗
fn copy_history_tracks_latest_visible_turn()

作用:这是一个测试,确认复制历史会按可见用户轮数正确保留最近仍然可见的助手回复。它防止回滚后复制到错误回答。

数据流:进去的是一个空状态;测试先记录第一轮用户和助手回答,再记录第二轮用户和助手回答;然后把历史裁剪回第一轮;出来用断言检查最近助手回复变回 first,并且没有错误标记为历史被完全淘汰。

调用关系:它只在测试时运行。它把 record_visible_user_turn、record_agent_markdown 和 truncate_copy_history_to_user_turn_count 串起来,验证这几个函数合在一起能正确支持“回滚后复制上一条可见回答”的场景。

调用图:外部调用 3 个(assert!, assert_eq!, default)。

tui/src/chatwidget/user_messages.rs源码 ↗
domain_logic用户输入、消息排队、恢复草稿、提交后显示历史记录时活跃

聊天里用户发出的东西不一定只有一段文字,还可能带本地图片、远程图片、粘贴片段、提到的文件或对象。这个文件就像聊天输入的“整理台”:先把这些内容装进统一的 UserMessage;如果多条草稿要合并,就重新编号图片占位符,避免好几张图都叫“Image #1”;如果历史记录里显示的文字和真正发给模型的文字不一样,也把这份差异保存好。它还负责把内部消息转换成界面上能显示的一行内容,并过滤掉不该显示的提示上下文。一个重要点是,它非常小心地维护 text_elements,也就是“文字中某一段有特殊含义”的位置范围;每次拼接文字时都会把范围往后挪对位置,避免点错、删错或显示错。

函数细节24
QueuedUserMessage::new67–73 ↗
fn new(user_message: UserMessage, action: QueuedInputAction) -> Self

作用:把一条用户消息包装成“排队等待发送”的消息,并附上这条消息要执行的输入动作。这样聊天组件可以先收下用户输入,等合适的时候再处理。

数据流:进去的是一条 UserMessage 和一个 QueuedInputAction;函数把它们放进 QueuedUserMessage 里,并把 pending_pastes 初始化为空;出来的是一条完整的排队消息。

调用关系:这是创建排队消息的基础入口。QueuedUserMessage::from 也会借它把普通消息变成默认的普通排队消息,后续弹出队列或提交消息时会用到这种包装。

调用图:外部调用 1 个(new)。

QueuedUserMessage::into_user_message75–77 ↗
fn into_user_message(self) -> UserMessage

作用:把排队消息外面的“排队包装”拆掉,只拿回里面真正的用户消息。适合在不再关心排队动作时使用。

数据流:进去的是一个 QueuedUserMessage 本身;函数取出其中的 user_message;出来的是 UserMessage,原来的排队对象被消耗掉。

调用关系:它位于排队流程的末端。当某段流程只需要消息内容、不需要动作和粘贴状态时,就会用它把数据还原成普通用户消息。

QueuedUserMessage::from81–83 ↗
fn from(user_message: UserMessage) -> Self

作用:让普通 UserMessage 可以直接变成 QueuedUserMessage,而且默认动作是普通输入。这样调用方不用每次都手写“Plain”这个默认动作。

数据流:进去的是一条 UserMessage;函数调用 QueuedUserMessage::new,并填入 QueuedInputAction::Plain;出来的是一条默认普通动作的排队消息。

调用关系:它是一个便捷转换。pop_next_queued_user_message 和 submit_user_message_with_history_and_shell_escape_policy 等流程会用它,把临时得到的用户消息快速塞进排队系统。

调用图:被 2 处调用(pop_next_queued_user_message, submit_user_message_with_history_and_shell_escape_policy);外部调用 1 个(new)。

QueuedUserMessage::deref89–91 ↗
fn deref(&self) -> &Self::Target

作用:让 QueuedUserMessage 在很多场景下可以像 UserMessage 一样被读取。也就是说,外层虽然是排队消息,但读内容时不用绕一大圈。

数据流:进去的是对 QueuedUserMessage 的引用;函数返回里面 user_message 的引用;它不改动任何数据。

调用关系:这是 Rust 的 Deref 机制,也就是“自动借用里面那层”的小帮手。它让其他代码读取排队消息内容时更自然。

ThreadComposerState::has_content111–118 ↗
fn has_content(&self) -> bool

作用:判断当前输入框草稿里是不是真的有东西。哪怕没有文字,只要有图片、特殊文字元素、提及绑定或待处理粘贴,也算有内容。

数据流:进去的是输入框状态;函数逐项检查 text、local_images、remote_image_urls、text_elements、mention_bindings、pending_pastes 是否为空;出来的是 true 或 false。

调用关系:它通常用于决定要不要恢复草稿、清空提示、阻止丢失输入等。它不处理内容本身,只负责回答“这个草稿是不是空的”。

UserMessage::from152–161 ↗
fn from(text: &str) -> Self

作用:把一段普通字符串快速变成 UserMessage。适合测试或简单输入,因为这种消息只有文字,没有图片和特殊标记。

数据流:进去的是 String 或 &str;函数把它放进 text 字段,并把图片、远程图片、文字元素、提及绑定都设为空;出来的是一条最简单的 UserMessage。

调用关系:这是很多流程的便捷入口。恢复输入、清空队列、预览队列、编辑最近消息、打断后恢复草稿等地方都会用它来构造简单消息。

调用图:被 22 处调用(side_restore_user_message_puts_inline_question_back_in_composer, clear_resets_all_input_queues, preview_keeps_queue_categories_separate, alt_up_edits_most_recent_queued_message, interrupt_prepends_queued_messages_before_existing_composer_text, interrupt_restores_queued_messages_into_composer, output_free_interrupted_turn_requests_prompt_restore, patch_activity_prevents_cancelled_turn_prompt_restore, thinking_status_keeps_cancelled_turn_prompt_restore_eligible, unbound_queued_message_edit_does_not_fall_back_to_alt_up (+12 more));外部调用 1 个(new)。

create_initial_user_message171–196 ↗
fn create_initial_user_message(
    text: Option<String>,
    local_image_paths: Vec<PathBuf>,
    text_elements: Vec<TextElement>,
) -> Option<UserMessage>

作用:根据启动时或外部传入的文字和本地图片路径,创建第一条用户消息。如果既没有文字也没有图片,就不创建,避免产生空消息。

数据流:进去的是可选文字、一组本地图片路径、一组文字元素;函数给每张本地图片分配类似“Image #1”的占位标签;出来的是 Some(UserMessage),或者在没有内容时出来 None。

调用关系:它用在最开始准备用户输入的时候。它会调用 local_image_label_text 生成图片标签,让后面的显示、提交和合并逻辑能用同一套占位符识别图片。

调用图:外部调用 1 个(new)。

append_text_with_rebased_elements198–211 ↗
fn append_text_with_rebased_elements(
    target_text: &mut String,
    target_text_elements: &mut Vec<TextElement>,
    text: &str,
    text_elements: impl IntoIterator<Item = TextElement>,
)

作用:把一段新文字接到已有文字后面,同时把这段文字里的特殊标记位置整体往后挪。否则拼接后,标记还指向旧位置,就会错位。

数据流:进去的是目标文字、目标文字元素、新文字和新文字元素;函数先记录目标文字当前长度,把新文字追加进去,再给新元素的 byte_range 加上这个偏移;出来是被原地更新的文字和元素列表。

调用关系:这是拼接消息时的关键小工具。user_message_display_from_inputs 和 merge_remapped_user_messages 都靠它保证文字元素的位置跟拼接后的大文本对得上。

调用图:被 2 处调用(user_message_display_from_inputs, merge_remapped_user_messages);外部调用 1 个(into_iter)。

app_server_text_elements213–215 ↗
fn app_server_text_elements(elements: &[TextElement]) -> Vec<AppServerTextElement>

作用:把 TUI 内部使用的文字元素转换成 app-server 需要的文字元素格式。app-server 可以理解成应用后台服务,它使用自己的协议类型。

数据流:进去的是一组 TextElement;函数逐个克隆并转换成 AppServerTextElement;出来的是后台服务协议能接收的列表。

调用关系:它是界面层和后台协议之间的翻译器。其他提交输入的流程需要把内部标记交给后台时,会使用这个函数做格式转换。

调用图:外部调用 1 个(iter)。

build_placeholder_mapping217–233 ↗
fn build_placeholder_mapping(
    local_images: Vec<LocalImageAttachment>,
    next_label: &mut usize,
) -> (HashMap<String, String>, Vec<LocalImageAttachment>)

作用:给一批本地图片重新分配连续的新占位符,并记录旧标签到新标签的对应关系。这样合并多条草稿时,不会出现多个“Image #1”。

数据流:进去的是本地图片列表和下一个图片编号;函数逐张图片生成新标签,更新编号,建立旧标签到新标签的 mapping,并生成改好标签的新图片列表;出来的是映射表和新图片列表。

调用关系:它服务于 remap_placeholders_for_message_and_history_record。后者会拿这张“旧名到新名”的表去改正文和历史显示文字。

调用图:调用 1 个内部函数(local_image_label_text);被 1 处调用(remap_placeholders_for_message_and_history_record);外部调用 2 个(new, new)。

remap_placeholders_in_text235–280 ↗
fn remap_placeholders_in_text(
    text: String,
    text_elements: Vec<TextElement>,
    mapping: &HashMap<String, String>,
) -> (String, Vec<TextElement>)

作用:按照给定的映射表,把文字里的图片或粘贴占位符替换成新名字,同时修正对应文字元素的位置范围。

数据流:进去的是原文字、原文字元素、旧占位符到新占位符的映射;函数按元素位置重建整段文字,遇到需要替换的占位符就换掉,并重新计算 byte_range;出来的是新文字和新文字元素。

调用关系:这是重命名占位符的核心工具。remap_colliding_paste_placeholders 和 remap_placeholders_for_message_and_history_record 都把具体的替换任务交给它。

调用图:被 2 处调用(remap_colliding_paste_placeholders, remap_placeholders_for_message_and_history_record);外部调用 2 个(new, new)。

remap_colliding_paste_placeholders282–308 ↗
fn remap_colliding_paste_placeholders(
    mut message: UserMessage,
    mut pending_pastes: Vec<(String, String)>,
    used: &mut HashSet<String>,
) -> (UserMessage, Vec<(String, String)>)

作用:处理粘贴内容占位符重名的问题。比如两段粘贴都叫“Pasted Content 100 chars”,它会给后来的那个加编号,避免界面和恢复逻辑分不清。

数据流:进去的是一条消息、一组待处理粘贴占位符和已使用占位符集合;函数检查每个占位符是否重复,重复就生成带“#2、#3”的新名字,并同步改消息正文里的占位符;出来的是改好的消息和粘贴列表,同时 used 集合也被更新。

调用关系:它在 drain_pending_messages_for_restore 这类恢复流程里使用。恢复多条待处理消息时,它先消除粘贴占位符冲突,再让草稿回到输入框。

调用图:调用 1 个内部函数(remap_placeholders_in_text);被 1 处调用(drain_pending_messages_for_restore);外部调用 2 个(new, format!)。

remap_placeholders_for_message_and_history_record315–351 ↗
fn remap_placeholders_for_message_and_history_record(
    message: UserMessage,
    history_record: UserMessageHistoryRecord,
    next_label: &mut usize,
) -> (UserMessage, UserMessageHistoryRecord)

作用:同时修正一条用户消息和它对应历史记录里的图片占位符。这样真正的消息、恢复草稿、历史显示三者不会说法不一致。

数据流:进去的是一条 UserMessage、一条 UserMessageHistoryRecord、下一个图片编号;函数先给本地图片重新编号,再把正文和可能存在的历史覆盖文字都按新编号替换;出来的是改好的消息和历史记录。

调用关系:它是图片占位符重排的主流程。测试辅助 remap_placeholders_for_message 会调用它,批量合并函数 remap_user_messages_with_history_records 也依赖它逐条处理。

调用图:调用 2 个内部函数(build_placeholder_mapping, remap_placeholders_in_text);被 1 处调用(remap_placeholders_for_message);外部调用 1 个(Override)。

remap_placeholders_for_message354–364 ↗
fn remap_placeholders_for_message(
    message: UserMessage,
    next_label: &mut usize,
) -> UserMessage

作用:这是测试用的小入口,只重排一条消息里的图片占位符,不关心历史记录。它让测试可以直接验证占位符重命名是否正确。

数据流:进去的是一条 UserMessage 和下一个图片编号;函数用默认的 UserMessageText 历史记录调用 remap_placeholders_for_message_and_history_record;出来的是改好占位符的 UserMessage。

调用关系:它只在测试配置下编译。它把真正工作交给 remap_placeholders_for_message_and_history_record,自己只是让测试代码更方便。

调用图:调用 1 个内部函数(remap_placeholders_for_message_and_history_record)。

remap_user_messages_with_history_records366–384 ↗
fn remap_user_messages_with_history_records(
    messages: Vec<(UserMessage, UserMessageHistoryRecord)>,
) -> Vec<(UserMessage, UserMessageHistoryRecord)>

作用:批量处理多条消息,让它们的图片占位符在合并前变成全局连续编号。远程图片也会被计入编号起点,避免本地图片编号和远程图片显示顺序打架。

数据流:进去的是多组“消息加历史记录”;函数先统计所有远程图片数量,把本地图片编号从这个数量之后开始,然后逐条重排占位符;出来的是改好编号的消息和历史记录列表。

调用关系:它是合并前的准备步骤。merge_user_messages 和 merge_user_messages_with_history_record 都会先调用它,再真正拼接文字和附件。

调用图:被 2 处调用(merge_user_messages, merge_user_messages_with_history_record)。

merge_user_messages386–394 ↗
fn merge_user_messages(messages: Vec<UserMessage>) -> UserMessage

作用:把多条用户消息合成一条。常见场景是用户打断任务后,有多条排队草稿需要合并回一个输入内容。

数据流:进去的是 UserMessage 列表;函数先给每条消息配默认历史记录并重排占位符,再调用 merge_remapped_user_messages 做实际拼接;出来的是一条合并后的 UserMessage。

调用关系:它是“不需要保留特殊历史显示规则”时的合并入口。真正拼接工作交给 merge_remapped_user_messages。

调用图:调用 2 个内部函数(merge_remapped_user_messages, remap_user_messages_with_history_records)。

merge_remapped_user_messages396–428 ↗
fn merge_remapped_user_messages(messages: impl IntoIterator<Item = UserMessage>) -> UserMessage

作用:把已经重排好占位符的消息真正拼接成一条。它会在不同消息之间加换行,并合并文字、图片、远程图片和提及绑定。

数据流:进去的是一批已经处理好的 UserMessage;函数从空消息开始,逐条追加文字,追加时修正文字元素位置,再把各种附件列表接到一起;出来的是一条完整的合并消息。

调用关系:它是底层合并工人。merge_user_messages 和 merge_user_messages_with_history_record 都先做好编号和历史记录准备,然后把最终拼接交给它。

调用图:调用 1 个内部函数(append_text_with_rebased_elements);被 2 处调用(merge_user_messages, merge_user_messages_with_history_record);外部调用 3 个(into_iter, new, new)。

user_message_for_restore430–444 ↗
fn user_message_for_restore(
    message: UserMessage,
    history_record: &UserMessageHistoryRecord,
) -> UserMessage

作用:根据历史记录决定恢复到输入框里的文字该用哪一份。如果历史记录提供了覆盖文字,就用覆盖文字;否则用消息原文。

数据流:进去的是一条 UserMessage 和它的历史记录;函数检查历史记录是否是非空 Override;如果是,就替换 text 和 text_elements,其他图片等内容保留;出来的是用于恢复的 UserMessage。

调用关系:它服务于 user_message_display_for_history。显示历史或恢复输入时,需要先确定用户应该看到哪段文字。

调用图:被 1 处调用(user_message_display_for_history)。

user_message_preview_text446–458 ↗
fn user_message_preview_text(
    message: &UserMessage,
    history_record: Option<&UserMessageHistoryRecord>,
) -> String

作用:取出一条用户消息在预览里应该显示的文字。它只关心文字,不处理图片和其他附件。

数据流:进去的是消息和可选历史记录;函数优先返回非空历史覆盖文字,否则返回消息自己的 text;出来的是一段用于预览的字符串。

调用关系:它常用于队列预览、历史预览这类轻量场景。相比 user_message_display_for_history,它不构建完整显示对象,只给出文字。

user_message_display_for_history460–475 ↗
fn user_message_display_for_history(
    message: UserMessage,
    history_record: &UserMessageHistoryRecord,
) -> UserMessageDisplay

作用:把一条用户消息变成聊天历史里可以显示的数据。它会尊重历史覆盖文字,并把本地图片从附件对象变成文件路径列表。

数据流:进去的是 UserMessage 和历史记录;函数先调用 user_message_for_restore 确定显示文字,再调用 ChatWidget::user_message_display_from_parts 生成 UserMessageDisplay;出来的是界面渲染需要的显示对象。

调用关系:on_committed_user_message 在用户消息真正进入历史时会用它。它把“内部保存格式”转成“界面显示格式”。

调用图:调用 1 个内部函数(user_message_for_restore);被 1 处调用(on_committed_user_message);外部调用 1 个(user_message_display_from_parts)。

merge_user_messages_with_history_record477–524 ↗
fn merge_user_messages_with_history_record(
    messages: Vec<(UserMessage, UserMessageHistoryRecord)>,
) -> (UserMessage, UserMessageHistoryRecord)

作用:把多条“消息加历史记录”合成一条消息和一条新的历史记录。它比 merge_user_messages 更谨慎,因为它要保留“历史里该怎么显示”的特殊规则。

数据流:进去的是多组 UserMessage 和 UserMessageHistoryRecord;函数先重排占位符,如果所有记录都只是普通原文,就保留普通记录;否则逐段拼出一份新的覆盖历史文字;最后合并消息本体;出来的是合并后的 UserMessage 和对应历史记录。

调用关系:它用于需要同时维护真实消息和历史显示的合并场景。它调用 remap_user_messages_with_history_records 做编号准备,再调用 merge_remapped_user_messages 合并消息正文和附件。

调用图:调用 2 个内部函数(merge_remapped_user_messages, remap_user_messages_with_history_records);外部调用 3 个(new, new, Override)。

ChatWidget::user_message_display_from_parts541–575 ↗
fn user_message_display_from_parts(
        message: String,
        text_elements: Vec<TextElement>,
        local_images: Vec<PathBuf>,
        remote_image_urls: Vec<String>,
    ) -> UserMessageDi

作用:把文字、文字元素、本地图片路径、远程图片地址组装成界面可显示的 UserMessageDisplay。它还会去掉提示上下文里不该显示的前缀,只保留真正的用户请求。

数据流:进去的是消息文字、文字元素、本地图片路径和远程图片地址;函数调用 extract_prompt_request_with_offset 找出真正要展示的那段文字,再过滤并平移落在这段文字内的元素范围;出来的是 UserMessageDisplay。

调用关系:这是显示转换的核心出口。user_message_display_for_history 和 user_message_display_from_inputs 都会把准备好的材料交给它,统一得到聊天界面能渲染的数据。

调用图:外部调用 1 个(extract_prompt_request_with_offset)。

ChatWidget::pending_steer_compare_key_from_items581–599 ↗
fn pending_steer_compare_key_from_items(
        items: &[UserInput],
    ) -> PendingSteerCompareKey

作用:为“等待确认的 steer 消息”生成一个简单对比钥匙。steer 可以理解成用户临时插入的一次指令;这个钥匙用来判断后台稍后返回的消息是不是同一条。

数据流:进去的是一组 UserInput;函数把所有文本拼成一个字符串,并统计图片数量,忽略技能和提及项;出来的是 PendingSteerCompareKey,里面只有文本和图片总数。

调用关系:它避免走昂贵的完整请求序列化流程。提交 pending steer 时用它先记下简化特征,等 app-server 提交后的 UserMessage 出现时再匹配,防止历史里重复显示。

调用图:外部调用 1 个(new)。

ChatWidget::user_message_display_from_inputs601–640 ↗
fn user_message_display_from_inputs(items: &[UserInput]) -> UserMessageDisplay

作用:把 app-server 协议里的用户输入项转换成聊天界面显示对象。输入项可能是一段文字、一张远程图片、一张本地图片,也可能是技能或提及。

数据流:进去的是 UserInput 列表;函数依次收集文字、远程图片 URL、本地图片路径和文字元素,拼接文字时会修正元素位置;最后调用 user_message_display_from_parts;出来的是 UserMessageDisplay。

调用关系:它是后台输入格式到 TUI 显示格式的桥梁。它把协议里的零散输入项整理好,再交给 ChatWidget::user_message_display_from_parts 做统一的显示裁剪和封装。

调用图:调用 1 个内部函数(append_text_with_rebased_elements);外部调用 3 个(user_message_display_from_parts, new, new)。

tui/src/chatwidget/command_lifecycle.rs源码 ↗
orchestrationrequest handling / main loop

这个文件是 ChatWidget 的一组事件处理器,像前台接待员一样,把来自后台的命令事件整理成用户能看懂的界面内容。命令开始时,它会决定要不要显示一个新的执行卡片,或者把它并进当前正在显示的执行组。命令输出一段文字时,它会把文字追加到对应卡片里,并要求界面重画。命令结束时,它会补上退出状态、耗时和完整输出,再决定这个卡片是留在当前区域还是放进历史记录。这里还特别照顾“统一执行”这类后台终端任务:它会在底部状态栏显示正在跑的命令、等待后台终端时给出提示,并避免重复的等待记录刷屏。整体作用就是让命令执行过程清楚、有序、不串台。

函数细节11
ChatWidget::flush_unified_exec_wait_streak9–18 ↗
fn flush_unified_exec_wait_streak(&mut self)

作用:把之前攒着的一段“正在等待后台终端”的提示正式写进聊天历史。这样等待状态不会一直悬空,也不会在后面真正有输入或命令结束时丢失。

数据流:进去的是 ChatWidget 当前保存的 unified_exec_wait_streak,也就是一段等待记录;如果没有等待记录,就什么都不做。它拿到等待记录后,创建一个统一执行交互的历史单元,把它发送给界面事件通道,并恢复推理状态标题;出来的结果是等待记录被清空,历史里多了一条等待交互记录。

调用关系:它通常由 ChatWidget::on_terminal_interaction 和 ChatWidget::on_command_execution_completed 调用。也就是说,当用户后来真的输入了内容,或者后台命令结束了,它会先把之前那段“等待”补进历史,避免状态断档。

调用图:被 2 处调用(on_command_execution_completed, on_terminal_interaction);外部调用 4 个(new, new, InsertHistoryCell, new_unified_exec_interaction)。

ChatWidget::on_command_execution_started20–52 ↗
fn on_command_execution_started(&mut self, item: ThreadItem)

作用:收到“某个命令开始执行”的消息后,先做入口判断和分流。它决定这个命令是只更新底部状态,还是要继续交给真正的开始渲染逻辑处理。

数据流:进去的是一个 ThreadItem,如果它不是命令执行事件,就直接返回。它从事件里取出命令、编号、进程号和来源;如果是统一执行来源,会记录这个进程、保持底部状态提示可见,并过滤掉不适合显示成标准工具调用的命令;最后把事件复制一份,交给延迟队列或立即处理。出来的结果是界面状态可能更新,命令开始事件可能被排队或立即渲染。

调用关系:这是命令开始事件的外层入口。它会调用 ChatWidget::track_unified_exec_process_begin 记录统一执行进程,然后通过 ChatWidget 自己的 defer_or_handle 机制,把活交给 ChatWidget::handle_command_execution_started_now 或等待之后再处理。

调用图:调用 1 个内部函数(track_unified_exec_process_begin);外部调用 1 个(clone)。

ChatWidget::on_exec_command_output_delta54–73 ↗
fn on_exec_command_output_delta(&mut self, call_id: &str, delta: &str)

作用:处理命令运行过程中不断冒出来的新输出,比如终端里新打印的一小段文字。它让用户能看到输出实时增加,而不是等命令结束后才一次性出现。

数据流:进去的是命令调用编号 call_id 和新增文本 delta。它先把这段输出记录到统一执行进程的最近输出里;如果当前没有任务在跑,或者当前活动卡片不是命令执行卡片,就停止。否则它把 delta 追加到对应执行卡片,成功后更新卡片版本并请求界面重画。出来的结果是对应命令卡片多了新输出,底部记录也可能更新。

调用关系:它是输出增量事件的入口,会先调用 ChatWidget::track_unified_exec_output_chunk 保存最近几行输出。之后它直接更新当前 ExecCell,也就是界面上正在显示的命令执行卡片。

调用图:调用 1 个内部函数(track_unified_exec_output_chunk)。

ChatWidget::on_terminal_interaction75–132 ↗
fn on_terminal_interaction(&mut self, process_id: String, stdin: String)

作用:处理和后台终端的交互,既包括用户真的输入了内容,也包括系统只是去轮询后台有没有新输出。它把“等待后台终端”和“用户输入终端”这两种情况分开显示。

数据流:进去的是进程编号 process_id 和标准输入 stdin。它先确认当前确实有任务在跑,再尝试找到这个进程对应的命令显示名。如果 stdin 为空,它把这次交互当作“正在等待后台终端”,更新底部状态栏,并把等待记录合并进 unified_exec_wait_streak;如果 stdin 不为空,它会先冲掉同一进程的等待记录,再把这次终端输入写进历史。出来的结果是底部状态栏或历史记录被更新。

调用关系:当终端交互事件到来时会走这里。它在需要收尾等待记录时调用 ChatWidget::flush_unified_exec_wait_streak,并用 new_unified_exec_interaction 创建历史交互单元,让等待和实际输入在界面上保持顺序。

调用图:调用 2 个内部函数(flush_unified_exec_wait_streak, new);外部调用 1 个(new_unified_exec_interaction)。

ChatWidget::on_command_execution_completed134–163 ↗
fn on_command_execution_completed(&mut self, item: ThreadItem)

作用:收到“命令执行结束”的消息后做外层收尾。它先处理统一执行的特殊状态,再把结束事件交给真正的完成渲染逻辑。

数据流:进去的是一个 ThreadItem,如果不是命令执行事件就直接返回。它取出调用编号、进程号和来源;如果是统一执行命令,会在必要时冲掉等待记录,并从正在跟踪的统一执行进程列表里移除它;如果界面当前没有任务在跑,就不再渲染。最后它复制事件并交给延迟队列或立即完成处理。出来的结果是统一执行状态被清理,命令完成事件可能被排队或渲染。

调用关系:它是命令结束事件的外层入口。它会调用 ChatWidget::flush_unified_exec_wait_streak 清理等待提示,调用 ChatWidget::track_unified_exec_process_end 移除进程,随后把事件交给 ChatWidget::handle_command_execution_completed_now 或延迟处理。

调用图:调用 2 个内部函数(flush_unified_exec_wait_streak, track_unified_exec_process_end);外部调用 1 个(clone)。

ChatWidget::track_unified_exec_process_begin165–191 ↗
fn track_unified_exec_process_begin(
        &mut self,
        call_id: &str,
        process_id: Option<&str>,
        command: &str,
    )

作用:记录一个统一执行进程已经开始了,并准备好它在底部状态栏里的显示信息。这样底部可以告诉用户现在有哪些后台命令还在运行。

数据流:进去的是调用编号 call_id、可选的进程号 process_id 和原始命令字符串 command。它用进程号或调用编号当作唯一钥匙,把命令拆开并整理成更适合显示的文字;如果这个进程已经存在,就更新它的调用编号、显示名并清空旧输出;如果不存在,就新增一条进程摘要。最后同步到底部栏。出来的结果是 unified_exec_processes 里有了最新进程状态。

调用关系:它由 ChatWidget::on_command_execution_started 在统一执行启动时调用。处理完内部记录后,它会调用 ChatWidget::sync_unified_exec_footer,让底部状态栏马上反映新进程。

调用图:调用 1 个内部函数(sync_unified_exec_footer);被 1 处调用(on_command_execution_started);外部调用 1 个(new)。

ChatWidget::track_unified_exec_process_end193–205 ↗
fn track_unified_exec_process_end(
        &mut self,
        call_id: &str,
        process_id: Option<&str>,
    )

作用:记录一个统一执行进程已经结束,并把它从“正在运行”的列表里拿掉。这样底部状态栏不会继续显示已经结束的命令。

数据流:进去的是调用编号 call_id 和可选进程号 process_id。它用进程号或调用编号找到对应记录,把它从 unified_exec_processes 中删除;如果列表真的变了,就同步底部栏。出来的结果是运行中进程列表减少,底部显示也可能更新。

调用关系:它由 ChatWidget::on_command_execution_completed 调用,是统一执行命令结束时的清理步骤。清理后它会调用 ChatWidget::sync_unified_exec_footer,把新的进程列表交给底部区域。

调用图:调用 1 个内部函数(sync_unified_exec_footer);被 1 处调用(on_command_execution_completed)。

ChatWidget::track_unified_exec_output_chunk217–240 ↗
fn track_unified_exec_output_chunk(&mut self, call_id: &str, chunk: &[u8])

作用:为统一执行底部提示保存最近几行输出。它不是保存完整日志,而是留一点最新动静,方便用户知道后台命令还在干什么。

数据流:进去的是调用编号 call_id 和一段字节形式的输出 chunk。它先找到对应统一执行进程,再把字节尽量转成文字,按行切开,去掉空行和行尾空白,然后追加到 recent_chunks。最后它只保留最多 3 条最新内容,旧的会被丢掉。出来的结果是对应进程的最近输出摘要被更新。

调用关系:它由 ChatWidget::on_exec_command_output_delta 调用,是每次收到命令输出片段时的附带记录步骤。它不直接画界面,但为统一执行底部区域提供最新输出材料。

调用图:被 1 处调用(on_exec_command_output_delta);外部调用 1 个(from_utf8_lossy)。

ChatWidget::handle_command_execution_started_now242–313 ↗
fn handle_command_execution_started_now(&mut self, item: ThreadItem)

作用:真正把“命令开始执行”显示到界面上。它会创建或扩展一个命令执行卡片,让用户看到新命令已经开始跑了。

数据流:进去的是一个命令执行 ThreadItem。它记录本轮有可见活动,解析命令和动作,确保底部状态指示器可见,把这个命令放进 running_commands 这张“正在运行命令表”里;如果它是重复的统一执行等待命令,就记为被抑制并不显示。否则它尝试把命令加入当前活动 ExecCell;如果加不了,就先冲掉旧活动卡片,再创建新的执行卡片。出来的结果是界面上出现或更新一个正在执行的命令卡片,并请求重画。

调用关系:它通常由 ChatWidget::on_command_execution_started 通过 defer_or_handle 调到。它负责开始阶段的实际渲染,并为后面的 ChatWidget::handle_command_execution_completed_now 留下 running_commands 记录,好让结束事件能找到开始时的命令信息。

调用图:调用 1 个内部函数(new);外部调用 2 个(new, matches!)。

ChatWidget::handle_command_execution_completed_now323–458 ↗
fn handle_command_execution_completed_now(&mut self, item: ThreadItem)

作用:真正把“命令执行结束”落到界面上,补齐输出、退出码和耗时。它还小心处理一个重要问题:不能把一个命令的结束结果错误合并到另一个还在运行的命令组里。

数据流:进去的是一个命令执行结束 ThreadItem。它取出命令、来源、完整输出、退出码和耗时,并从 running_commands 里找开始时保存的信息;如果这个调用之前被抑制显示,就直接结束。接着它判断结束结果该落到哪里:当前活动执行卡片、一个单独的历史卡片,或一个新卡片。然后它创建 CommandOutput,完成对应调用,必要时刷新活动卡片、插入历史或请求重画。最后标记这轮确实做过工作;如果是用户 shell,还可能发送下一条排队输入。

调用关系:它通常由 ChatWidget::on_command_execution_completed 通过 defer_or_handle 调到,是命令生命周期的收尾核心。它会根据当前 ExecCell 的状态决定是否完成现有卡片,或者用 InsertHistoryCell 插入一个孤立的完成记录,避免不同命令的界面记录互相污染。

调用图:外部调用 6 个(new, from_millis, new, InsertHistoryCell, debug_assert!, matches!)。

tui/src/chatwidget/hook_lifecycle.rs源码 ↗
orchestrationmain loop / UI event handling

聊天界面不只显示用户和助手的文字,还会显示一些后台自动任务的状态。这个文件就是 ChatWidget 里负责这些状态的“小调度台”。它会在 hook 开始时创建或更新一个临时显示块,在 hook 完成时把结果转成正式历史记录,还会在合适的时候清掉空的临时块,避免界面上留下半截状态。它还处理动画和计时:如果某个 hook 还在跑,或者某些内容到了该显示的时间,就请求下一帧刷新。这里重要的一点是,正在运行的 hook 和已经完成、应该进入聊天历史的 hook 被分开处理。这样用户既能看到实时进度,又不会把临时状态误写进永久历史。

函数细节7
ChatWidget::clear_active_hook_cell10–15 ↗
fn clear_active_hook_cell(&mut self)

作用:清掉当前正在界面上临时显示的 hook 状态,但不把它写进聊天历史。常用于需要丢弃临时进度、让界面回到干净状态的时候。

数据流:进去的是 ChatWidget 自己当前保存的 active_hook_cell,也就是正在显示的 hook 临时块。它尝试把这个临时块拿走;如果原来确实有内容,就更新活动块的版本号,并请求稍后插入用量信息。出来没有返回值,但界面状态会从“有临时 hook 显示”变成“没有临时 hook 显示”。

调用关系:这是一个清理动作,不把工作交给别的 hook 生命周期函数。它的位置像是手动擦掉黑板上的临时提示:擦完后通知界面这块内容变了,并让后续的用量显示有机会补上。

ChatWidget::on_hook_started17–35 ↗
fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary)

作用:当一个 hook 开始运行时更新聊天界面,让用户看到有后台小任务正在执行。它还会先把之前该收尾的内容收好,避免新旧状态混在一起。

数据流:输入是一个 HookRunSummary,也就是这次 hook 运行的简要信息。函数先记录当前轮次有可见活动,再把正在流式输出的答案用分隔线收住,并调用 ChatWidget::flush_completed_hook_output 把已经完成的 hook 结果写入历史。然后它看当前有没有 active_hook_cell:有就把新运行加入这个显示块;没有就通过外部的 new_active_hook_cell 创建一个新的活动显示块。最后更新版本并请求重画界面。

调用关系:它通常在系统收到“hook 已开始”的事件时被调用。它先交给 ChatWidget::flush_completed_hook_output 清理已完成内容,再根据需要创建新的活动 hook 显示块,最后请求界面刷新,让用户马上看到运行状态。

调用图:调用 1 个内部函数(flush_completed_hook_output);外部调用 1 个(new_active_hook_cell)。

ChatWidget::on_hook_completed37–67 ↗
fn on_hook_completed(
        &mut self,
        completed: codex_app_server_protocol::HookRunSummary,
    )

作用:当一个 hook 结束时,把它从“正在运行”变成“已完成”,并决定它是继续暂存在当前显示块里,还是写进聊天历史。这样界面不会一直显示一个已经结束的任务。

数据流:输入是完成后的 HookRunSummary。函数先尝试在当前 active_hook_cell 里找到对应的运行并标记完成;如果找到了,就只更新这个块。如果没找到,它会把这个完成记录加到现有块里,或者在没有现有块时用外部的 new_completed_hook_cell 新建一个已完成显示块。之后它调用 ChatWidget::flush_completed_hook_output,把可以永久保存的完成结果送进历史;再调用 ChatWidget::finish_active_hook_cell_if_idle,检查临时块是不是已经可以收尾。最后请求重画。

调用关系:它通常在系统收到“hook 已完成”的事件时运行。它会把具体收尾工作分给 ChatWidget::flush_completed_hook_output 和 ChatWidget::finish_active_hook_cell_if_idle,自己负责把完成事件接到正确的界面状态上。

调用图:调用 2 个内部函数(finish_active_hook_cell_if_idle, flush_completed_hook_output);外部调用 1 个(new_completed_hook_cell)。

ChatWidget::flush_completed_hook_output69–89 ↗
fn flush_completed_hook_output(&mut self)

作用:把当前 hook 显示块里已经完成、并且应该长期保留的结果取出来,插入聊天历史。简单说,就是把“临时进度条里的完成内容”转成“正式聊天记录”。

数据流:它读取 active_hook_cell,并从里面取出已完成且可持久保存的 hook 运行内容。如果没有这种内容,就什么也不做。如果取出了 completed_cell,它会检查原来的活动块是否空了;空了就清掉 active_hook_cell。接着更新版本,标记聊天记录需要最终消息分隔线,通过 AppEvent::InsertHistoryCell 把这个历史块发给应用事件通道,并请求稍后插入用量输出。

调用关系:它被 ChatWidget::on_hook_started 和 ChatWidget::on_hook_completed 调用。前者在新 hook 开始前用它清掉旧的完成结果,后者在 hook 完成后用它把结果正式放入历史。它通过 AppEvent::InsertHistoryCell 把内容交给应用的历史插入流程。

调用图:被 2 处调用(on_hook_completed, on_hook_started);外部调用 2 个(new, InsertHistoryCell)。

ChatWidget::finish_active_hook_cell_if_idle91–110 ↗
fn finish_active_hook_cell_if_idle(&mut self)

作用:检查当前 hook 临时显示块是不是已经没事可做了;如果空了就删除,如果到了该写入历史的时候就写入历史。它防止界面上残留已经结束的临时状态。

数据流:它先读取 active_hook_cell。没有活动块就直接结束。如果这个块是空的,就清掉它、更新版本,并请求补充用量输出。如果这个块虽然不空,但它自己判断 should_flush 为真,意思是到了该落入历史的时机,函数就把整个块拿走,标记需要分隔线,通过 AppEvent::InsertHistoryCell 插入历史,然后请求用量输出。结果是活动区域被清空或保留,历史可能新增一块 hook 内容。

调用关系:它被 ChatWidget::on_hook_completed 和 ChatWidget::update_due_hook_visibility 调用。前者在 hook 刚完成后检查是否能收尾,后者在时间推进后检查是否到了该收尾的时刻。它把真正插入历史的动作交给应用事件 AppEvent::InsertHistoryCell。

调用图:被 2 处调用(on_hook_completed, update_due_hook_visibility);外部调用 2 个(new, InsertHistoryCell)。

ChatWidget::update_due_hook_visibility112–121 ↗
fn update_due_hook_visibility(&mut self)

作用:根据当前时间推进 hook 的可见状态,比如某些提示该出现了,或者动画该换一帧了。它让 hook 显示不会卡在旧状态。

数据流:它读取 active_hook_cell;如果没有活动 hook,就直接返回。如果有,它用 Instant::now 取得当前时间,把时间交给这个显示块的 advance_time。若显示块因此发生变化,就更新版本。最后调用 ChatWidget::finish_active_hook_cell_if_idle,顺手检查这个块是否已经可以清掉或写入历史。

调用关系:它通常由界面主循环或定时刷新触发。它自己负责“时间到了,显示要不要变”,然后把收尾判断交给 ChatWidget::finish_active_hook_cell_if_idle。

调用图:调用 1 个内部函数(finish_active_hook_cell_if_idle);外部调用 1 个(now)。

ChatWidget::schedule_hook_timer_if_needed123–143 ↗
fn schedule_hook_timer_if_needed(&self)

作用:如果当前 hook 还需要动画或定时显示变化,就安排下一次界面刷新。没有它,运行中的 hook 可能不会及时更新,看起来像卡住了。

数据流:它读取配置里的 animations 开关和 active_hook_cell 的状态。如果开了动画,并且有正在可见运行的 hook,就用 Duration::from_millis 安排 50 毫秒后刷新一帧。然后它再查看活动块有没有下一个定时截止点;如果有,就用 Instant::now 算出还要等多久,并让 frame_requester 在这个延迟后请求刷新。它不返回数据,但会向刷新调度器登记未来的重画请求。

调用关系:它通常在界面准备等待下一帧或下一次事件时被调用。它不直接改变 hook 内容,而是告诉 frame_requester:“请在合适的时候叫醒界面”,这样 ChatWidget::update_due_hook_visibility 之类的更新才有机会按时发生。

调用图:外部调用 2 个(from_millis, now)。

tui/src/chatwidget/turn_runtime.rs源码 ↗
orchestrationagent turn start, running, completion, error handling

可以把一次提问到 AI 回答完,看成餐厅里一张订单:下单、厨房忙碌、上菜、结账,任何一步出错都要有人收拾现场。这个文件就是 ChatWidget 里的“订单调度员”。它在任务开始时清空上一轮的临时内容,打开“正在工作”的提示;任务完成时把流式输出收尾,把最终分隔线、运行耗时、计划更新、桌面通知等放到正确位置;遇到限流、服务器过载、安全策略拦截等问题时,它会停止当前轮次,把错误写进历史,并尝试继续处理排队中的下一条用户输入。这里还特别注意一些边界情况,比如 MCP 启动也可能让底部栏显示“正在运行”,计划模式结束后可能弹出“要不要开始执行计划”的选择框,避免用户误以为系统空闲或漏掉下一步。

函数细节22
ChatWidget::update_task_running_state13–19 ↗
fn update_task_running_state(&mut self)

作用:刷新底部输入区的“正在运行”状态。它不只看 AI 这一轮是否还在跑,也看 MCP 启动是否还没结束,避免界面过早显示空闲。

数据流:它读取当前 AI 轮次是否运行、MCP 启动状态是否存在 → 算出底部栏应该显示忙还是闲 → 更新底部栏,同时刷新计划模式提示和状态显示。

调用关系:任务开始、任务正常结束、任务异常收尾时都会调用它。它像最后统一拨动状态灯的开关,保证其他流程改完内部状态后,界面也同步变亮或熄灭。

调用图:被 3 处调用(finalize_turn, on_task_complete, on_task_started)。

ChatWidget::collect_runtime_metrics_delta21–25 ↗
fn collect_runtime_metrics_delta(&mut self)

作用:从会话遥测里取出刚刚新增的运行指标。遥测就是系统偷偷记下的耗时、网络等统计,用来展示或排查问题。

数据流:它向 session_telemetry 询问有没有新的运行统计 → 如果有,就交给 apply_runtime_metrics_delta 合并 → 自己不直接显示结果,只负责把新数据取出来。

调用关系:refresh_runtime_metrics 会用它做手动刷新;on_task_complete 会在一轮结束时用它收集最后一批数据。它把收集到的活儿交给 apply_runtime_metrics_delta。

调用图:调用 1 个内部函数(apply_runtime_metrics_delta);被 2 处调用(on_task_complete, refresh_runtime_metrics)。

ChatWidget::apply_runtime_metrics_delta27–33 ↗
fn apply_runtime_metrics_delta(&mut self, delta: RuntimeMetricsSummary)

作用:把一小段新运行统计合并进当前轮次的总统计里。遇到 WebSocket 耗时数据时,还会把可读的摘要写进聊天历史。

数据流:输入是一份 RuntimeMetricsSummary,也就是运行统计摘要 → 它先判断里面有没有 WebSocket 计时信息,再合并到 turn_runtime_metrics → 如果值得展示,就调用 log_websocket_timing_totals 写一行历史记录。

调用关系:collect_runtime_metrics_delta 取到新统计后会调用它。它负责累计数据,并在需要时把展示任务交给 log_websocket_timing_totals。

调用图:调用 1 个内部函数(log_websocket_timing_totals);被 1 处调用(collect_runtime_metrics_delta)。

ChatWidget::log_websocket_timing_totals35–41 ↗
fn log_websocket_timing_totals(&mut self, delta: RuntimeMetricsSummary)

作用:把 WebSocket 的耗时统计写成一条用户能看到的灰色历史提示。WebSocket 可以理解成客户端和服务器之间保持打开的一条实时通信通道。

数据流:输入是一份运行统计摘要 → 它从里面提取 responses API 的计时摘要并转成短标签 → 如果能生成标签,就往历史里追加一行“WebSocket timing: ...”。

调用关系:apply_runtime_metrics_delta 判断有 WebSocket 计时数据时会调用它。它是统计数据从内部数字变成聊天历史可见文字的出口。

调用图:调用 1 个内部函数(responses_api_summary);被 1 处调用(apply_runtime_metrics_delta);外部调用 2 个(runtime_metrics_label, vec!)。

ChatWidget::refresh_runtime_metrics43–45 ↗
fn refresh_runtime_metrics(&mut self)

作用:触发一次运行指标刷新。它是一个简单入口,用来把新产生的统计数据收进当前轮次。

数据流:它没有额外输入 → 直接调用 collect_runtime_metrics_delta → 可能让当前轮次的统计数字增加,也可能没有变化。

调用关系:它把“刷新指标”这个动作包装成一个清晰的方法,实际工作交给 collect_runtime_metrics_delta。

调用图:调用 1 个内部函数(collect_runtime_metrics_delta)。

ChatWidget::on_task_started49–79 ↗
fn on_task_started(&mut self)

作用:在 AI 开始新一轮工作时初始化现场。它会清掉上一轮的临时状态,打开运行提示,并让界面进入“Working”。

数据流:它读取当前界面和队列状态 → 标记用户输入不再等待启动,记录开始时间,清空推理缓冲、运行统计、旧提示和临时流 → 更新底部栏、状态标题、宠物提示,并请求重画界面。

调用关系:这是每轮 AI 工作的开场动作。它会调用 update_task_running_state,让底部状态灯和新一轮生命周期保持一致。

调用图:调用 1 个内部函数(update_task_running_state);外部调用 3 个(now, from, default)。

ChatWidget::on_task_complete81–208 ↗
fn on_task_complete(
        &mut self,
        last_agent_message: Option<String>,
        duration_ms: Option<i64>,
        from_replay: bool,
    )

作用:在 AI 一轮工作正常结束时做完整收尾。它把最后回答、计划流、运行统计、通知、排队输入和下一步提示都处理好。

数据流:输入包括最后一条 AI 文本、耗时、是否来自回放 → 它清理待提交的 steer,整理可复制的最终回答,结束流式输出,收集运行指标,必要时加最终分隔线,清空运行命令和临时状态 → 然后刷新界面、可能弹出计划执行提示、发送下一条排队输入,或通知用户本轮完成。

调用关系:这是任务正常结束的总调度函数。它会调用 collect_runtime_metrics_delta、update_task_running_state、has_queued_follow_up_messages 和 maybe_prompt_plan_implementation,并在需要时发出 ConsolidateProposedPlan 事件让别处合并计划。

调用图:调用 6 个内部函数(agent_turn_preview, collect_runtime_metrics_delta, has_queued_follow_up_messages, maybe_prompt_plan_implementation, update_task_running_state, new);外部调用 2 个(ConsolidateProposedPlan, default)。

ChatWidget::maybe_prompt_plan_implementation210–235 ↗
fn maybe_prompt_plan_implementation(&mut self)

作用:判断是否该弹出“要不要执行刚才的计划”的提示。它只在计划模式刚产出计划、没有别的输入排队、也没有弹窗挡着时才会出现。

数据流:它读取协作模式是否开启、当前模式、是否有排队消息、本轮是否看到计划、底部栏是否空闲、限流提示是否挂起 → 所有条件都满足才打开计划执行选择框 → 否则什么都不做。

调用关系:on_task_complete 在一轮结束后调用它。它先用 has_queued_follow_up_messages 排除自动继续的情况,最终把真正打开弹窗的事交给 open_plan_implementation_prompt。

调用图:调用 2 个内部函数(has_queued_follow_up_messages, open_plan_implementation_prompt);被 1 处调用(on_task_complete);外部调用 1 个(matches!)。

ChatWidget::open_plan_implementation_prompt237–250 ↗
fn open_plan_implementation_prompt(&mut self)

作用:真正打开计划执行选择界面。这个界面让用户决定接下来用什么协作模式、是否带着已有上下文继续执行计划。

数据流:它读取模型目录、最新计划文本、上下文使用情况 → 组装选择界面所需参数 → 让底部栏显示选择视图,并发出一个计划模式提示通知。

调用关系:maybe_prompt_plan_implementation 判断时机合适后会调用它。它会向 default_mode_mask、plan_implementation_context_usage_label 和 selection_view_params 要信息来搭好弹窗。

调用图:调用 3 个内部函数(selection_view_params, plan_implementation_context_usage_label, default_mode_mask);被 1 处调用(maybe_prompt_plan_implementation)。

ChatWidget::plan_implementation_context_usage_label259–279 ↗
fn plan_implementation_context_usage_label(&self) -> Option<String>

作用:为计划执行提示生成“上下文用了多少”的短标签。上下文可以理解成 AI 还记得的对话容量,这个标签帮助用户判断要不要清理旧对话再执行计划。

数据流:它读取 token_info,也就是上下文容量和 token 使用信息;token 可以粗略理解成模型处理文字的计量单位 → 优先算已用百分比,没有百分比就算已用 token 数 → 如果几乎没用或无法判断,就返回空。

调用关系:open_plan_implementation_prompt 在生成提示框时会调用它。它只提供一句显示文字,不直接改界面。

调用图:被 1 处调用(open_plan_implementation_prompt);外部调用 1 个(format!)。

ChatWidget::has_queued_follow_up_messages281–283 ↗
fn has_queued_follow_up_messages(&self) -> bool

作用:检查是否已经有下一条用户消息在队列里等着发送。这样系统就知道结束当前轮后是不是马上要开下一轮。

数据流:它读取 input_queue 的排队状态 → 调用队列自己的判断方法 → 返回 true 或 false,不修改数据。

调用关系:on_task_complete 用它判断是否该通知用户“已完成”;maybe_prompt_plan_implementation 用它避免在马上要继续对话时弹出计划执行提示。

调用图:被 2 处调用(maybe_prompt_plan_implementation, on_task_complete)。

ChatWidget::handle_app_server_steer_rejected_error285–293 ↗
fn handle_app_server_steer_rejected_error(
        &mut self,
        codex_error_info: &AppServerCodexErrorInfo,
    ) -> bool

作用:处理服务器拒绝“steer”的情况。steer 可以理解成用户在 AI 工作中途追加的指令,如果当前轮不允许插话,就把它重新排队。

数据流:输入是服务器返回的错误信息 → 它判断错误是不是“当前轮不能被 steer” → 如果是,就尝试把被拒绝的 steer 放回队列,并返回是否成功。

调用关系:handle_non_retry_error 会在处理非重试错误时先给它机会。这样有些“错误”不会变成红色失败,而是转成稍后再发的排队输入。

调用图:外部调用 1 个(matches!)。

ChatWidget::finalize_turn299–326 ↗
fn finalize_turn(&mut self)

作用:强制结束当前轮并清理现场,通常用于取消或出错。它会把正在转圈的执行项变成失败状态,避免界面留下半截“还在跑”的东西。

数据流:它读取当前活动流、执行单元、临时 hook 行、运行命令等状态 → 丢掉只用于预览的流尾,把活动项标为失败,清空运行标记、缓冲和排队插入请求 → 刷新状态栏相关信息,必要时显示限流提示。

调用关系:on_error、on_cyber_policy_error 和 on_server_overloaded_error 都会先调用它收拾现场。它内部调用 update_task_running_state,但特意不清掉 MCP 启动状态,因为 MCP 可能还在独立运行。

调用图:调用 1 个内部函数(update_task_running_state);被 3 处调用(on_cyber_policy_error, on_error, on_server_overloaded_error)。

ChatWidget::on_server_overloaded_error328–341 ↗
fn on_server_overloaded_error(&mut self, message: String)

作用:处理服务器太忙的错误。它会结束当前轮,给用户一条温和的警告,并尝试继续发送后面排队的输入。

数据流:输入是一段服务器错误消息 → 它先关闭当前轮,如果消息为空就换成默认的“Codex 负载较高” → 把警告写入历史、请求重画,再尝试发送下一条排队输入。

调用关系:handle_non_retry_error 判断错误类型是服务器过载时会调用它。它把收尾工作交给 finalize_turn,把展示警告交给历史记录组件。

调用图:调用 1 个内部函数(finalize_turn);被 1 处调用(handle_non_retry_error);外部调用 1 个(new_warning_event)。

ChatWidget::on_error343–356 ↗
fn on_error(&mut self, message: String)

作用:处理普通错误并把它显示给用户。它会停止当前轮、写入红色错误记录、切换失败提示,然后看看队列里是否还能继续下一条。

数据流:输入是一段错误消息 → 它先结束回答流,再调用 finalize_turn 清理状态 → 把错误写进历史,设置失败宠物提示,刷新界面,并尝试发送下一条排队输入。

调用关系:handle_non_retry_error 的兜底路径会调用它,on_rate_limit_error 在多种限流情况下也会调用它。它是普通失败场景的统一出口。

调用图:调用 1 个内部函数(finalize_turn);被 2 处调用(handle_non_retry_error, on_rate_limit_error);外部调用 1 个(new_error_event)。

ChatWidget::on_cyber_policy_error358–366 ↗
fn on_cyber_policy_error(&mut self)

作用:处理网络安全策略拦截类错误。它会结束当前轮,并在历史里写入专门的安全策略提示。

数据流:它没有外部文本输入 → 清掉中途 steer 提交状态,调用 finalize_turn 收尾 → 添加一条 cyber policy 错误历史,刷新界面,再尝试发送下一条排队输入。

调用关系:handle_non_retry_error 识别到服务器返回的是安全策略错误时会调用它。它和 on_error 类似,但历史消息使用专门的安全策略说明。

调用图:调用 1 个内部函数(finalize_turn);被 1 处调用(handle_non_retry_error);外部调用 1 个(new_cyber_policy_error_event)。

ChatWidget::on_rate_limit_error368–411 ↗
fn on_rate_limit_error(&mut self, error_kind: RateLimitErrorKind, message: String)

作用:处理额度、用量上限或普通限流错误。它会根据具体身份和限制类型,显示不同的错误文案,有时还会提示成员去找工作区所有者加额度。

数据流:输入是限流错误类型和服务器消息 → 它先根据是否为用量上限修正内部的限流原因 → 再按工作区所有者、成员、额度耗尽、用量达到上限等情况选择错误提示 → 调用 on_error 显示错误,必要时打开给所有者的提醒弹窗。

调用关系:handle_non_retry_error 判断错误属于限流后会调用它。它不自己做底层收尾,而是复用 on_error 完成结束轮次和写历史。

调用图:调用 1 个内部函数(on_error);被 1 处调用(handle_non_retry_error);外部调用 1 个(matches!)。

ChatWidget::handle_non_retry_error413–440 ↗
fn handle_non_retry_error(
        &mut self,
        message: String,
        codex_error_info: Option<AppServerCodexErrorInfo>,
    )

作用:把“不再自动重试”的错误分门别类。不同错误需要不同处理方式,比如重新排队 steer、显示安全策略、提示限流,或作为普通错误结束。

数据流:输入是错误文字和可选的服务器错误详情 → 它先看是不是 steer 被拒并可重新排队,再看是不是安全策略错误,再看是不是过载或限流 → 按分类调用对应处理函数;都不是就调用普通 on_error。

调用关系:它是错误分发器,站在错误处理入口处。它会把活儿交给 on_cyber_policy_error、on_server_overloaded_error、on_rate_limit_error 或 on_error。

调用图:调用 4 个内部函数(on_cyber_policy_error, on_error, on_rate_limit_error, on_server_overloaded_error)。

ChatWidget::on_warning442–449 ↗
fn on_warning(&mut self, message: impl Into<String>)

作用:显示一条警告,但会避免重复刷屏。警告不是致命错误,只是提醒用户注意某个情况。

数据流:输入是一段可转成字符串的消息 → 它先问 warning_display_state 这条是否应该显示 → 如果应该,就把警告写入历史并请求重画;如果不该显示,直接返回。

调用关系:on_app_server_model_verification 会用它展示模型验证相关警告。它是警告消息进入聊天历史的统一小入口。

调用图:被 1 处调用(on_app_server_model_verification);外部调用 2 个(into, new_warning_event)。

ChatWidget::on_app_server_model_verification451–458 ↗
fn on_app_server_model_verification(
        &mut self,
        verifications: &[AppServerModelVerification],
    )

作用:处理服务器返回的模型验证结果。某些验证结果代表需要提醒用户,比如开启了 cyber 相关的可信访问。

数据流:输入是一组模型验证标记 → 它检查里面是否包含 TrustedAccessForCyber → 如果包含,就调用 on_warning 显示固定警告;否则不做事。

调用关系:它把服务器的验证标记翻译成用户能看懂的警告。真正写历史和刷新界面的动作交给 on_warning。

调用图:调用 1 个内部函数(on_warning);外部调用 1 个(contains)。

ChatWidget::on_plan_update460–474 ↗
fn on_plan_update(&mut self, update: UpdatePlanArgs)

作用:处理 AI 发来的计划进度更新。它会记录本轮确实出现了计划,并把完成了几步、总共几步更新到界面状态里。

数据流:输入是一份计划更新,里面有多个步骤和每步状态 → 它统计总步数和已完成步数,保存最近进度,刷新状态表面 → 再把计划更新作为一条历史内容加入聊天记录。

调用关系:当后端发来计划更新事件时会用它。它不调用本文件里的其他函数,但会影响 on_task_complete 后续是否可能弹出计划执行提示。

调用图:外部调用 1 个(new_plan_update)。

ChatWidget::interrupted_turn_message476–482 ↗
fn interrupted_turn_message(&self, reason: TurnAbortReason) -> String

作用:生成“这一轮被中断了”的提示文字。不同中断原因会给用户不同解释。

数据流:输入是中断原因 → 如果是预算达到上限,就返回“目标预算已到”的说明 → 其他情况返回通用的“对话已中断,可以告诉模型怎么改,也可反馈问题”的说明。

调用关系:它是一个小型文案选择器,供中断流程需要展示说明时使用。它不改状态,也不把工作交给别的函数。

tui/src/chatwidget/streaming.rs源码 ↗
domain_logicmain loop / streaming update handling

这个文件像聊天窗口的“字幕机”。模型的回答不是一次性来的,而是一小段一小段传进来;这里负责把这些小段先放进临时队列,按节奏刷到界面上,再在结束时合并成一条正式记录。它还处理计划模式的流式输出、推理过程里的状态标题、出错提示,以及流式输出期间状态栏该隐藏还是恢复。一个重要点是“尾巴单元”:还没正式写入历史记录的最后几行会作为临时内容显示,像直播字幕的最后一屏。等内容结束后,它会清掉临时尾巴,把内容整理进历史,并通知外层重新排版或停止动画。它也会延后处理中断事件,避免在文字还没写完时插入执行结果,导致顺序乱掉。

函数细节25
ChatWidget::restore_reasoning_status_header9–17 ↗
fn restore_reasoning_status_header(&mut self)

作用:把推理过程里的状态标题恢复到界面上。比如模型正在“思考”,它会优先从推理文字里找一个加粗标题来显示;找不到时,就退回显示“Working”。

数据流:进去的是当前聊天组件里的推理缓存和任务运行状态 → 它尝试从推理缓存中提取第一段加粗文字 → 出来的是状态栏标题被更新,同时终端标题状态也被标成“思考中”或“工作中”。

调用关系:这个函数会被外部流程调用,用来在推理状态被临时打断后重新摆正状态栏。它不创建新内容,只是根据已有缓存恢复用户能看到的提示。

调用图:外部调用 1 个(from)。

ChatWidget::flush_answer_stream_with_separator19–59 ↗
fn flush_answer_stream_with_separator(&mut self)

作用:结束当前助手回答的流式显示,把临时内容收尾并整理成正式历史记录。它还会在需要时触发重新排版,保证窗口大小变化后内容仍能正确显示。

数据流:进去的是当前的回答流控制器、临时尾巴、累计源码和界面状态 → 它清掉临时尾巴,取出最终单元和原始文本,把能直接加入历史的内容加入历史,把需要重排的内容发事件交给外层处理 → 出来的是流控制器被关闭、分块节奏被重置、动画可能停止,并安排后续用量信息插入。

调用关系:它由 ChatWidget::finalize_completed_assistant_message 在一条助手消息结束时调用。内部会用 ChatWidget::clear_active_stream_tail 清掉直播尾巴,也会问 ChatWidget::stream_controllers_idle 是否所有流都空了,以决定是否停止提交动画。

调用图:调用 2 个内部函数(clear_active_stream_tail, stream_controllers_idle);被 1 处调用(finalize_completed_assistant_message)。

ChatWidget::stream_controllers_idle61–71 ↗
fn stream_controllers_idle(&self) -> bool

作用:判断当前是否已经没有待刷新的流式内容了。简单说,就是看回答流和计划流的队列是不是都空了。

数据流:进去的是回答流控制器和计划流控制器的当前状态 → 它分别检查每个控制器还有没有排队的行 → 出来一个布尔值:true 表示都闲了,false 表示还有内容没显示完。

调用关系:它被 ChatWidget::flush_answer_stream_with_separator 和 ChatWidget::maybe_restore_status_indicator_after_stream_idle 使用。前者用它决定动画能不能停,后者用它决定状态栏能不能恢复。

调用图:被 2 处调用(flush_answer_stream_with_separator, maybe_restore_status_indicator_after_stream_idle)。

ChatWidget::maybe_restore_status_indicator_after_stream_idle79–95 ↗
fn maybe_restore_status_indicator_after_stream_idle(&mut self)

作用:在流式输出真正停下来以后,才把状态栏恢复出来。这样可以避免一边刷文字一边闪烁状态栏。

数据流:进去的是“是否等待恢复状态栏”的标记、任务是否还在跑、以及流队列是否空闲 → 条件都满足时,它重新显示状态指示器,并恢复之前保存的标题和详情 → 出来的是状态栏重新出现,等待恢复的标记被清掉。

调用关系:它会被 ChatWidget::on_agent_message_item_completed、ChatWidget::on_plan_item_completed 和 ChatWidget::run_commit_tick_with_scope 调用。也就是说,消息结束、计划结束、或每次刷流式内容后,都会尝试看看现在是否适合恢复状态栏。

调用图:调用 1 个内部函数(stream_controllers_idle);被 3 处调用(on_agent_message_item_completed, on_plan_item_completed, run_commit_tick_with_scope)。

ChatWidget::finalize_completed_assistant_message97–109 ↗
fn finalize_completed_assistant_message(&mut self, message: Option<&str>)

作用:处理一条助手回答正式完成的时刻。它会补上没通过流式过程显示的最终文本,然后统一收尾。

数据流:进去的是可选的最终消息文本 → 如果之前没有流控制器且文本不空,它先把这段文本当作流式增量处理;然后刷新并关闭回答流,处理流结束,最后请求重画界面 → 出来的是这条回答进入稳定状态,界面准备刷新。

调用关系:它由 ChatWidget::on_agent_message_item_completed 调用,是助手消息完成时的总收口。它会把具体工作交给 ChatWidget::handle_streaming_delta、ChatWidget::flush_answer_stream_with_separator 和 ChatWidget::handle_stream_finished。

调用图:调用 3 个内部函数(flush_answer_stream_with_separator, handle_stream_finished, handle_streaming_delta);被 1 处调用(on_agent_message_item_completed)。

ChatWidget::on_agent_message_delta111–113 ↗
fn on_agent_message_delta(&mut self, delta: String)

作用:接收助手回答的新片段。模型每吐出一小段文字,这里就把它送进流式显示流程。

数据流:进去的是一段新到的回答文字 → 它直接交给流式增量处理函数 → 出来的是这段文字被排队显示,界面随后会更新。

调用关系:这是外部事件进入回答流的入口之一。它本身很薄,只负责把活交给 ChatWidget::handle_streaming_delta。

调用图:调用 1 个内部函数(handle_streaming_delta)。

ChatWidget::on_plan_delta115–145 ↗
fn on_plan_delta(&mut self, delta: String)

作用:处理“计划模式”里新到的一小段计划文本。只有当前真的处在计划模式时,它才会显示这些内容。

数据流:进去的是计划文本片段和当前模式 → 如果不是计划模式就忽略;否则把片段追加到计划缓存,必要时新建计划流控制器,再把文本推入队列 → 出来的是计划内容开始或继续流式显示,动画可能启动,界面请求重画。

调用关系:它在计划流事件到来时使用。第一次收到计划内容时会先清理正在显示的执行单元,再创建 PlanStreamController;随后可能调用 ChatWidget::run_catch_up_commit_tick 追赶显示进度,并调用 ChatWidget::sync_active_stream_tail 更新临时尾巴。

调用图:调用 3 个内部函数(run_catch_up_commit_tick, sync_active_stream_tail, new)。

ChatWidget::on_plan_item_completed147–198 ↗
fn on_plan_item_completed(&mut self, text: String)

作用:处理一段计划输出完成的时刻,把临时计划内容变成正式历史记录。它还会记住最新计划,供后续流程使用。

数据流:进去的是最终计划文本,以及之前流式收到的计划缓存 → 它选择最终文本或流式缓存作为计划内容,记录到历史和最新计划字段;然后关闭计划流控制器,清理临时尾巴,必要时发出合并计划的事件 → 出来的是计划项结束,历史里出现正式计划,状态栏可能等待恢复。

调用关系:它在计划项完成事件到来时运行。它会调用 ChatWidget::clear_active_stream_tail 清临时尾巴,并在流队列空时通过 ChatWidget::maybe_restore_status_indicator_after_stream_idle 恢复状态栏;如果需要正式生成计划历史单元,会使用 new_proposed_plan。

调用图:调用 2 个内部函数(clear_active_stream_tail, maybe_restore_status_indicator_after_stream_idle);外部调用 2 个(ConsolidateProposedPlan, new_proposed_plan)。

ChatWidget::on_agent_reasoning_delta200–220 ↗
fn on_agent_reasoning_delta(&mut self, delta: String)

作用:处理模型推理过程中新到的文字,但不把它当作正式聊天回答显示。它主要用这些文字提取一个“正在思考什么”的状态标题。

数据流:进去的是一段推理文字 → 它追加到推理缓存;如果当前有执行等待提示,就不抢状态栏;否则尝试从推理内容中找第一段加粗文字作为标题 → 出来的是状态栏可能显示新的思考标题,界面请求重画。

调用关系:它处理推理流的增量事件。它不进入普通回答流,也不调用提交队列,而是只影响状态栏和推理缓存。

ChatWidget::on_agent_reasoning_final222–235 ↗
fn on_agent_reasoning_final(&mut self)

作用:处理一段推理内容结束的时刻,把累计推理内容保存成聊天记录里的推理摘要块。

数据流:进去的是当前推理缓存和完整推理缓存 → 它把当前块追加到完整缓存,如果不为空就创建一个推理摘要历史单元并加入历史 → 出来的是推理缓存被清空,界面请求重画。

调用关系:它在推理块最终完成时调用。它会使用 new_reasoning_summary_block 创建历史单元,让推理内容只作为摘要记录保存,而不是混进助手正式回答。

调用图:外部调用 1 个(new_reasoning_summary_block)。

ChatWidget::on_reasoning_section_break237–242 ↗
fn on_reasoning_section_break(&mut self)

作用:在推理内容分段时切开当前块。这样下一段推理可以重新提取自己的状态标题。

数据流:进去的是当前推理缓存 → 它把这段内容追加到完整推理缓存,并加上空行分隔,然后清空当前缓存 → 出来的是完整缓存保留前一段,当前缓存准备接收下一段。

调用关系:它在推理流出现章节分隔时使用。后续 ChatWidget::on_agent_reasoning_delta 会在新的空缓存上继续收集和提取标题。

ChatWidget::on_stream_error244–254 ↗
fn on_stream_error(&mut self, message: String, additional_details: Option<String>)

作用:显示流式输出出错时的提示。它会把状态栏切换成带错误信息的样子,让用户知道当前不是正常生成。

数据流:进去的是错误消息和可选的补充说明 → 它先记住重试相关的旧状态,再确保状态栏存在,设置终端标题状态,并把错误文本显示到状态栏 → 出来的是用户能看到错误提示,状态栏详情按规则首字母大写。

调用关系:它在流式过程发生错误时被调用。它不处理文本队列,而是直接更新状态显示,帮助用户理解当前卡住或失败的原因。

ChatWidget::on_agent_message_item_completed261–302 ↗
fn on_agent_message_item_completed(
        &mut self,
        item: AgentMessageItem,
        from_replay: bool,
    )

作用:处理一条助手消息完整到达后的总流程。它把消息内容拼起来、解析可见部分、完成流式收尾,并根据消息阶段决定是否记录正式回答。

数据流:进去的是一个助手消息项和“是否来自回放”的标记 → 它拼接所有文本内容,解析出可见 Markdown,再完成消息收尾;如果是最终回答就记录到对话历史;如果发现新建分支且不是回放,还会异步查询当前 Git 分支并同步线程信息 → 出来的是消息进入历史,状态恢复策略被更新。

调用关系:这是助手消息完成事件的主入口。它调用 ChatWidget::finalize_completed_assistant_message 做显示收尾,之后调用 ChatWidget::maybe_restore_status_indicator_after_stream_idle 尝试恢复状态栏;如果需要查分支,会通过异步任务调用 current_branch_name。

调用图:调用 3 个内部函数(current_branch_name, finalize_completed_assistant_message, maybe_restore_status_indicator_after_stream_idle);外部调用 4 个(from, new, matches!, spawn)。

ChatWidget::on_commit_tick306–308 ↗
fn on_commit_tick(&mut self)

作用:响应一次定时提交节拍。提交节拍可以理解成“每隔一小会儿把排队文字刷一点到屏幕上”。

数据流:进去的是当前聊天组件状态 → 它调用常规提交函数 → 出来的是流式队列可能被推进,界面内容可能多显示几行。

调用关系:它是外部定时器进入本文件的入口。真正的提交逻辑交给 ChatWidget::run_commit_tick。

调用图:调用 1 个内部函数(run_commit_tick)。

ChatWidget::run_commit_tick311–313 ↗
fn run_commit_tick(&mut self)

作用:执行一次普通的流式提交。普通模式会按较平滑的节奏把内容写到历史区。

数据流:进去的是当前流控制器和分块状态 → 它用“任意模式都可提交”的范围调用底层提交函数 → 出来的是队列可能被消耗,部分内容进入历史显示。

调用关系:它由 ChatWidget::on_commit_tick 调用,是常规动画节拍的下一层。实际计算和更新交给 ChatWidget::run_commit_tick_with_scope。

调用图:调用 1 个内部函数(run_commit_tick_with_scope);被 1 处调用(on_commit_tick)。

ChatWidget::run_catch_up_commit_tick316–318 ↗
fn run_catch_up_commit_tick(&mut self)

作用:在内容积压时执行一次“追赶式”提交。它只在追赶模式有效,目的是别让显示落后模型输出太多。

数据流:进去的是当前流式队列状态 → 它用“只允许追赶模式提交”的范围调用底层提交函数 → 出来的是如果确实在追赶,就会一次刷更多内容;否则不硬刷。

调用关系:它会被 ChatWidget::handle_streaming_delta 和 ChatWidget::on_plan_delta 调用。每当新内容入队且需要启动动画时,它会先尝试追赶一波,减少延迟。

调用图:调用 1 个内部函数(run_commit_tick_with_scope);被 2 处调用(handle_streaming_delta, on_plan_delta)。

ChatWidget::run_commit_tick_with_scope326–349 ↗
fn run_commit_tick_with_scope(&mut self, scope: CommitTickScope)

作用:真正执行一次流式提交,把队列里的若干行变成历史记录里的正式单元。它还负责隐藏或恢复状态栏、停止动画、刷新运行指标。

数据流:进去的是提交范围、当前时间、回答流控制器、计划流控制器和自适应分块状态 → 它调用底层提交算法拿到要加入历史的单元和队列状态;把这些单元加入历史,更新临时尾巴;如果所有队列空了,就尝试恢复状态栏并停止动画 → 出来的是界面历史推进,尾巴同步,运行指标可能刷新。

调用关系:它是 ChatWidget::run_commit_tick 和 ChatWidget::run_catch_up_commit_tick 的共同核心。提交后它会调用 ChatWidget::sync_active_stream_tail 保持临时尾巴正确,也会调用 ChatWidget::maybe_restore_status_indicator_after_stream_idle 做收尾恢复。

调用图:调用 2 个内部函数(maybe_restore_status_indicator_after_stream_idle, sync_active_stream_tail);被 2 处调用(run_catch_up_commit_tick, run_commit_tick);外部调用 1 个(now)。

ChatWidget::flush_interrupt_queue351–355 ↗
fn flush_interrupt_queue(&mut self)

作用:把之前因为流式写入而暂时排队的中断事件一次性处理掉。这样可以保证事件顺序不乱。

数据流:进去的是当前中断管理器和聊天组件 → 它先把中断管理器临时取出来,逐个处理里面排队的事件,再放回组件 → 出来的是中断队列被清空或推进,相关界面变化被应用。

调用关系:它由 ChatWidget::handle_stream_finished 调用。也就是说,只有等非执行类的流式内容插入完,才会处理那些被延后的事件。

调用图:被 1 处调用(handle_stream_finished);外部调用 1 个(take)。

ChatWidget::defer_or_handle358–371 ↗
fn defer_or_handle(
        &mut self,
        push: impl FnOnce(&mut InterruptManager),
        handle: impl FnOnce(&mut Self),
    )

作用:决定一个中断事件是现在处理,还是先排队等流式输出结束。它的核心目标是保持显示顺序稳定。

数据流:进去的是两个动作:一个负责把事件放入中断队列,另一个负责马上处理事件 → 如果当前有回答流正在写,或者队列里已经有东西,就继续排队;否则立刻处理 → 出来的是事件要么被延后,要么即时影响界面。

调用关系:它是中断处理的分流器。虽然这里没有直接列出调用者,但它服务于那些可能和流式文字交错出现的事件,避免出现“结束事件跑到开始事件前面”这种反直觉顺序。

ChatWidget::handle_stream_finished373–380 ↗
fn handle_stream_finished(&mut self)

作用:处理流式内容结束后的清理工作。它会隐藏某些完成状态,并释放之前被延后的中断事件。

数据流:进去的是任务完成等待标记和中断队列 → 如果任务完成标记存在,它隐藏状态指示器并清掉标记;然后刷新中断队列 → 出来的是收尾状态被整理,排队事件开始处理。

调用关系:它由 ChatWidget::finalize_completed_assistant_message 在回答收尾后调用。它接着调用 ChatWidget::flush_interrupt_queue,让被延后的执行开始、结束等事件按顺序落地。

调用图:调用 1 个内部函数(flush_interrupt_queue);被 1 处调用(finalize_completed_assistant_message)。

ChatWidget::handle_streaming_delta383–416 ↗
fn handle_streaming_delta(&mut self, delta: String)

作用:处理助手回答的新文字片段,是普通回答流式显示的核心入口。它负责开流、入队、启动动画、更新临时尾巴。

数据流:进去的是一段回答增量文本 → 如果不空就记录本轮有可见活动;如果还没有流控制器,就先清理执行单元和必要的分隔线,再创建新的 StreamController;然后把文本推入控制器 → 出来的是内容进入显示队列,动画可能启动,临时尾巴同步,界面请求重画。

调用关系:它被 ChatWidget::on_agent_message_delta 直接调用,也会被 ChatWidget::finalize_completed_assistant_message 用来补处理最终消息。新内容入队后,它可能调用 ChatWidget::run_catch_up_commit_tick 追赶显示,并调用 ChatWidget::sync_active_stream_tail 更新屏幕上的最后几行。

调用图:调用 4 个内部函数(run_catch_up_commit_tick, sync_active_stream_tail, new, new);被 2 处调用(finalize_completed_assistant_message, on_agent_message_delta)。

ChatWidget::active_cell_is_stream_tail418–423 ↗
fn active_cell_is_stream_tail(&self) -> bool

作用:判断当前活动单元是不是流式显示的临时尾巴。临时尾巴就是还没正式写进历史、但先显示给用户看的最后几行。

数据流:进去的是当前活动单元 → 它检查这个单元的真实类型是不是助手流尾巴或计划流尾巴 → 出来一个布尔值,表示它是否可以被当作流尾巴清理或复用。

调用关系:它被 ChatWidget::clear_active_stream_tail 和 ChatWidget::has_active_stream_tail 调用。前者用它避免误删普通活动单元,后者用它确认当前确实有流尾巴。

调用图:被 2 处调用(clear_active_stream_tail, has_active_stream_tail)。

ChatWidget::has_active_stream_tail425–428 ↗
fn has_active_stream_tail(&self) -> bool

作用:判断界面上是否真的存在一个正在显示的流式临时尾巴。它同时要求有流控制器,并且活动单元确实是尾巴类型。

数据流:进去的是当前回答流控制器、计划流控制器和活动单元 → 它先看是否有任一流控制器存在,再调用类型检查 → 出来一个布尔值,说明屏幕上是否有活跃流尾巴。

调用关系:它调用 ChatWidget::active_cell_is_stream_tail。外部可以用它判断当前是不是处在“直播字幕还挂着”的状态。

调用图:调用 1 个内部函数(active_cell_is_stream_tail)。

ChatWidget::sync_active_stream_tail430–465 ↗
fn sync_active_stream_tail(&mut self)

作用:把屏幕上的临时尾巴同步成最新状态。每次流式队列推进后,最后几行都要重新对齐。

数据流:进去的是回答流控制器或计划流控制器的当前尾部行 → 如果有回答尾巴,就创建助手尾巴单元;否则如果有计划尾巴,就创建计划尾巴单元;如果都没有,就清掉尾巴 → 出来的是活动单元变成最新临时尾巴,或被清空,同时状态栏会在有尾巴时隐藏。

调用关系:它被 ChatWidget::handle_streaming_delta、ChatWidget::on_plan_delta 和 ChatWidget::run_commit_tick_with_scope 调用。它也会在没有尾部内容时调用 ChatWidget::clear_active_stream_tail 做清理。

调用图:调用 3 个内部函数(clear_active_stream_tail, new, new);被 3 处调用(handle_streaming_delta, on_plan_delta, run_commit_tick_with_scope);外部调用 1 个(new)。

ChatWidget::clear_active_stream_tail467–472 ↗
fn clear_active_stream_tail(&mut self)

作用:清掉当前活动的流式临时尾巴,但不会误删普通内容。它是流结束或尾巴为空时的安全清理动作。

数据流:进去的是当前活动单元 → 它先确认这个单元确实是流尾巴;如果是,就把活动单元设为空并更新版本号 → 出来的是临时尾巴从界面状态里移除。

调用关系:它被 ChatWidget::flush_answer_stream_with_separator、ChatWidget::on_plan_item_completed 和 ChatWidget::sync_active_stream_tail 调用。内部会先用 ChatWidget::active_cell_is_stream_tail 做保护,避免清错东西。

调用图:调用 1 个内部函数(active_cell_is_stream_tail);被 3 处调用(flush_answer_stream_with_separator, on_plan_item_completed, sync_active_stream_tail)。

tui/src/app/agent_message_consolidation.rs源码 ↗
domain_logicagent message finalized / UI reflow

智能体回答时,文字是一点点流出来的。为了让界面看起来顺滑,聊天窗口会先放入一些临时的 AgentMessageCell,可以理解成“还没定稿的小纸条”。但回答结束后,聊天记录不能一直靠这些小纸条保存,因为以后窗口大小变化时,需要用完整的 Markdown 原文重新排版。这个文件做的事,就是在回答最终完成时,从聊天记录末尾往前找出属于这次回答的一串临时消息,把它们替换成一个正式的 AgentMarkdownCell。这个正式单元保存原始文本和当前目录,成为之后重绘、滚动、调整大小时的可靠来源。如果界面上正打开 transcript 覆盖层,它也会同步替换那里的内容,并请求下一帧刷新。最后,它还会根据本次是否需要重新排版,收尾处理流式显示留下的滚动区域状态。

函数细节2
App::handle_consolidate_agent_message24–74 ↗
fn handle_consolidate_agent_message(
        &mut self,
        tui: &mut tui::Tui,
        source: String,
        cwd: PathBuf,
        scrollback_reflow: ConsolidationScrollbackReflow,
        defe

作用:这个函数在智能体回答结束时被调用,用来把刚才流式显示出来的临时消息,合并成一条正式、可长期保存和重排的消息。没有这一步,聊天记录末尾会留下很多临时片段,之后窗口 resize 时就不容易按原文正确重画。

数据流:进去的是界面对象 tui、完整回答原文 source、当前工作目录 cwd、一次滚动区重排策略 scrollback_reflow,以及可能延后加入的一格历史内容 deferred_history_cell。它先把延后的历史格子补进主聊天记录和 transcript 覆盖层;然后从 transcript_cells 末尾往前找连续的临时 AgentMessageCell;如果找到了,就用 source 和 cwd 新建一个正式的 AgentMarkdownCell,把那一段临时格子整体替换掉,并同步更新覆盖层、请求界面刷新;最后根据重排策略完成收尾。出来的结果是聊天记录从“多段临时显示”变成“一条正式消息”,必要时界面也被安排重新绘制。

调用关系:它是这份文件的主流程入口,通常发生在一次流式回答结束之后。它会创建 AgentMarkdownCell,并在需要时调用 App::finish_agent_message_consolidation 继续处理滚动区重排的收尾;如果没有找到可合并的临时消息,它就直接走 maybe_finish_stream_reflow 这条较轻的收尾路线。它还会和 transcript 覆盖层、TUI 的 frame requester 配合,保证数据变了之后屏幕能及时刷新。

调用图:调用 2 个内部函数(finish_agent_message_consolidation, new);外部调用 4 个(new, frame_requester, once, debug!)。

App::finish_agent_message_consolidation76–91 ↗
fn finish_agent_message_consolidation(
        &mut self,
        tui: &mut tui::Tui,
        scrollback_reflow: ConsolidationScrollbackReflow,
    ) -> Result<()>

作用:这个函数专门处理“合并消息之后,滚动区要不要重新排版、怎么收尾”的决定。它把合并本身和重排收尾分开,让主流程更清楚。

数据流:进去的是当前 App、界面对象 tui,以及 scrollback_reflow 这个重排要求。它查看这个要求:如果只是“之前 resize 重排跑过才需要收尾”,就调用 maybe_finish_stream_reflow;如果明确要求必须完成重排,就调用 finish_required_stream_reflow。出来没有新的数据值,主要效果是把界面的流式滚动区域状态整理到正确状态。

调用关系:它只由 App::handle_consolidate_agent_message 调用,是主合并流程最后的收尾小步骤。它不负责替换聊天记录内容,只负责根据 ConsolidationScrollbackReflow 的指示,把后续工作交给合适的重排完成函数。

调用图:被 1 处调用(handle_consolidate_agent_message)。

tui/src/transcript_reflow.rs源码 ↗
domain_logicmain loop / resize handling / streaming

终端不像网页那样保存一棵可随时重排的界面树。文字一旦写进终端,终端就按当时的宽度把它折成多行。窗口大小变了,旧的滚动历史不会自动按新宽度变漂亮,所以 Codex 要从自己内存里的“原始记录”重新生成一遍。这个文件不负责真正画字,也不负责清屏;它像一个排队叫号器,只记录“要不要重排”“等到什么时候再重排”“上次真正按哪个宽度修过”。它还特别照顾模型正在流式输出的情况:流式输出就是文字一边生成一边显示,这时内容还没最终归档。如果 resize 发生在这段时间,它会记下,等输出变成正式历史后,再补一次基于正式内容的重排,避免把临时换行永久留在滚动历史里。

函数细节26
TranscriptReflowState::clear42–44 ↗
fn clear(&mut self)

作用:把整个重排状态清空,回到刚启动时的样子。通常在应用丢掉旧 transcript,也就是旧聊天显示记录不再可信时使用。

数据流:进去的是当前这份状态对象 → 它用默认值覆盖所有字段,包括记住的宽度、等待中的截止时间、流式输出标记 → 出来的是一份干净状态,不再会拿旧记录触发重排。

调用关系:它会调用默认值构造能力来重置自己。上层在销毁或替换 transcript 状态时会用它,避免后面的绘制流程误以为旧内容还需要修。

调用图:外部调用 1 个(default)。

TranscriptReflowState::note_width51–60 ↗
fn note_width(&mut self, width: u16) -> TranscriptWidthChange

作用:记录这次绘制时看到的终端宽度,并告诉调用方这是第一次看到宽度,还是宽度真的变了。

数据流:进去一个宽度数字 → 它把这个宽度存成“最近观察到的宽度”,并拿旧值比较;如果这是第一次观察,还把它当作已经重排过的基准宽度 → 出来一个 TranscriptWidthChange,里面说明是否初始化、是否变化。

调用关系:绘制过程每次知道终端宽度时会先调用它。它不直接安排重排,只提供判断依据,后续流程再决定要不要调用 schedule_debounced。

TranscriptReflowState::reflow_needed_for_width68–70 ↗
fn reflow_needed_for_width(&self, width: u16) -> bool

作用:判断某个宽度下的滚动历史是不是还没修好。它看的不是“最近看到的宽度”,而是“最近真正重排过的宽度”。

数据流:进去一个目标宽度 → 它检查这个宽度是否既不是上次真正重排的宽度,也不是已经排队等待重排的宽度 → 出来 true 或 false,表示还要不要安排一次修复。

调用关系:resize 后的绘制流程会用它避免重复排队。同一个宽度如果已经在等重排,就不会再反复安排。

TranscriptReflowState::schedule_debounced78–85 ↗
fn schedule_debounced(&mut self, target_width: Option<u16>) -> bool

作用:安排一次“稍等一下再做”的重排。这样用户拖动窗口边缘时,不会每一像素变化都重排一次,而是等短暂安静后按最终宽度重排。

数据流:进去一个可选目标宽度;有宽度就记为等待修复的宽度 → 它读取当前时间,并把截止时间设为当前时间加 75 毫秒 → 返回 false,表示这次安排本身不要求立刻执行。

调用关系:上层在发现 resize 时会调用它。它调用系统时间 now 来设置延迟,真正到点以后由 pending_is_due 帮绘制流程判断。

调用图:外部调用 1 个(now)。

TranscriptReflowState::schedule_immediate91–94 ↗
fn schedule_immediate(&mut self)

作用:安排一次尽快执行的重排。它用于不能再等 75 毫秒的场景,比如流式输出刚归档,需要马上把临时换行换成正式历史的换行。

数据流:进去的是当前状态 → 它清掉等待中的目标宽度,并把截止时间设成现在 → 出来是一份“已经到期”的待重排状态。

调用关系:流式输出结束并发现必须补修时,上层会用它。它调用 now 取当前时间,之后 pending_is_due 会立刻认为这项任务可以做。

调用图:外部调用 1 个(now)。

TranscriptReflowState::set_due_for_test97–99 ↗
fn set_due_for_test(&mut self)

作用:这是测试专用的小工具,用来把等待中的重排伪造成“已经过期”。正常运行代码不会用它。

数据流:进去的是测试里的状态对象 → 它取当前时间,再减去 1 毫秒,作为 pending_until → 出来的是一个马上应该执行重排的测试状态。

调用关系:测试用它制造边界情况,比如旧的等待时间已经到了又重新安排。它会调用 now 和 from_millis 这些时间工具。

调用图:外部调用 2 个(from_millis, now)。

TranscriptReflowState::pending_is_due101–103 ↗
fn pending_is_due(&self, now: Instant) -> bool

作用:检查等待中的重排是否已经到时间了。简单说,就是看闹钟响没响。

数据流:进去一个当前时间 → 它拿这个时间和内部保存的截止时间比较;如果没有截止时间就认为没到期 → 出来 true 或 false。

调用关系:绘制循环或 resize 修复流程会用它决定现在是否该真正重排。它只做判断,不清除任务,也不执行重排。

TranscriptReflowState::pending_until105–107 ↗
fn pending_until(&self) -> Option<Instant>

作用:把当前等待重排的截止时间拿出来给别人看。常用于调度和测试。

数据流:进去的是状态对象 → 它读取内部 pending_until 字段 → 出来一个可能存在的时间点;没有待办时就是空。

调用关系:测试会用它确认 debounce,也就是延迟等待,确实被往后推了。运行时也可以用它知道下一次该在什么时候检查。

TranscriptReflowState::has_pending_reflow109–111 ↗
fn has_pending_reflow(&self) -> bool

作用:快速回答:现在有没有一项重排任务在等着做。

数据流:进去的是状态对象 → 它查看 pending_until 是否有值 → 出来 true 或 false。

调用关系:上层流程可以用它决定是否需要继续关注重排任务。它不关心任务何时到期,只关心有没有。

TranscriptReflowState::clear_pending_reflow113–116 ↗
fn clear_pending_reflow(&mut self)

作用:清掉当前等待中的重排任务。适合在任务已经处理完,或者决定不要做这次修复时调用。

数据流:进去的是当前状态 → 它把截止时间和等待中的目标宽度都设为空 → 出来后,同一宽度如果仍需要修复,可以重新安排。

调用关系:真正完成重排后,上层通常会清掉待办。它也配合 reflow_needed_for_width:清掉后,同一个目标宽度不再被视为“已经排队”。

TranscriptReflowState::mark_reflowed_width123–125 ↗
fn mark_reflowed_width(&mut self, width: u16) -> bool

作用:记录滚动历史刚刚实际按哪个宽度重排过。它还能告诉调用方,这个宽度和上次记录相比是不是新的。

数据流:进去一个实际重排宽度 → 它替换内部 last_reflow_width → 出来 true 表示宽度变了,false 表示本来就记录的是这个宽度。

调用关系:真正执行重排的代码在完成后会调用它。它让后续 reflow_needed_for_width 能区分“只是观察到宽度”和“已经按这个宽度修过”。

TranscriptReflowState::mark_ran_during_stream132–134 ↗
fn mark_ran_during_stream(&mut self)

作用:记下:刚才有一次重排发生在流式输出还没最终归档的时候。这意味着之后可能还要补一次正式重排。

数据流:进去的是状态对象 → 它把 ran_during_stream 标记设为 true → 出来后,take_stream_finish_reflow_needed 会知道流结束时需要检查并触发补修。

调用关系:重排执行者如果发现自己是在流式输出期间工作,会调用它。它不安排时间,只留下一个必须善后的标记。

TranscriptReflowState::mark_resize_requested_during_stream142–144 ↗
fn mark_resize_requested_during_stream(&mut self)

作用:记下:窗口大小变化发生在流式输出期间,或者临时输出还没合并成正式历史时。即使当时 debounce 没到点,流结束后也不能忘了补修。

数据流:进去的是状态对象 → 它把 resize_requested_during_stream 标记设为 true → 出来后,状态保留“流结束后需要重排”的线索。

调用关系:resize 调度流程在检测到流式输出相关状态时会调用它。它和 mark_ran_during_stream 一起保证流结束后最多补一次基于正式内容的重排。

TranscriptReflowState::take_stream_finish_reflow_needed151–156 ↗
fn take_stream_finish_reflow_needed(&mut self) -> bool

作用:在流式输出完成时询问:是否需要补一次正式重排。它是“取走式读取”:问完就把相关标记清掉。

数据流:进去的是当前状态 → 它查看 ran_during_stream 和 resize_requested_during_stream 两个标记,只要有一个为真就认为需要补修;然后把两个标记都清零 → 出来 true 或 false。

调用关系:流式输出合并成正式历史后,上层会调用它。如果返回 true,上层通常会安排 schedule_immediate;因为标记会被清掉,所以同一轮事件不会重复触发。

TranscriptReflowState::clear_stream_flags162–165 ↗
fn clear_stream_flags(&mut self)

作用:只清掉和流式输出补修有关的标记,不碰宽度和等待中的重排任务。

数据流:进去的是状态对象 → 它把 ran_during_stream 和 resize_requested_during_stream 设为 false → 出来后,宽度记录、debounce 截止时间等仍保持原样。

调用关系:当一次必要的流结束补修已经完成时,上层会用它善后。它比 clear 更温和,不会把整个 resize 状态机重置成第一次绘制。

tests::schedule_debounced_postpones_existing_reflow182–195 ↗
fn schedule_debounced_postpones_existing_reflow()

作用:测试连续安排 debounce 重排时,第二次会把截止时间往后推。也就是模拟用户还在拖窗口,系统应该再等等。

数据流:进去的是一个默认状态 → 测试先安排一次延迟重排,记下截止时间,睡 1 毫秒后再安排一次 → 出来通过断言确认新的截止时间更晚。

调用关系:它调用默认构造、sleep、from_millis 和断言工具来验证 schedule_debounced 的核心行为:连续 resize 不应太早重排。

调用图:外部调用 4 个(from_millis, assert!, sleep, default)。

tests::schedule_debounced_postpones_due_existing_reflow198–208 ↗
fn schedule_debounced_postpones_due_existing_reflow()

作用:测试即使旧的重排已经到期,再次 resize 也会重新开始一段安静等待期。

数据流:进去的是默认状态 → 测试先用 set_due_for_test 做出“已经到期”的任务,再记录当前时间并重新安排 debounce → 出来通过断言确认新截止时间在当前时间之后。

调用关系:它用 now 和断言工具检查 schedule_debounced。这个测试保护拖动窗口时的体验:新 resize 事件会刷新等待时间。

调用图:外部调用 3 个(now, assert!, default)。

tests::first_observed_width_marks_reflow_baseline211–220 ↗
fn first_observed_width_marks_reflow_baseline()

作用:测试第一次看到终端宽度时,只建立基准,不应该误认为需要重排旧历史。

数据流:进去的是默认状态 → 调用 note_width 记录 80 列 → 出来通过断言确认 initialized 为真、观察宽度和重排宽度都是 80,并且 80 列不需要重排。

调用关系:它验证 note_width 和 reflow_needed_for_width 的配合。这样第一帧绘制不会做多余的滚动历史修复。

调用图:外部调用 3 个(assert!, assert_eq!, default)。

tests::mark_reflowed_width_records_actual_rebuild_width223–231 ↗
fn mark_reflowed_width_records_actual_rebuild_width()

作用:测试记录“实际重排宽度”时,不会改掉“最近观察到的宽度”。两者必须分开记。

数据流:进去的是默认状态 → 先观察 80 列,再标记实际按 100 列重排 → 出来确认观察宽度仍是 80,而重排宽度变成 100。

调用关系:它验证 mark_reflowed_width 的设计重点。终端可能先后报告不同宽度,所以调度流程必须知道到底哪个宽度已经真正修过。

调用图:外部调用 3 个(assert!, assert_eq!, default)。

tests::reflow_needed_compares_against_actual_rebuild_width234–241 ↗
fn reflow_needed_compares_against_actual_rebuild_width()

作用:测试是否需要重排时,要和“实际修过的宽度”比较,而不是只看最近观察到的宽度。

数据流:进去的是默认状态 → 先观察 80,标记实际修到 90,再观察 100 → 出来断言 100 仍然需要重排。

调用关系:它保护 reflow_needed_for_width 的关键逻辑。没有这个区分,终端最终宽度可能已经被看见,却没有真正修复滚动历史。

调用图:外部调用 2 个(assert!, default)。

tests::pending_reflow_target_prevents_repeated_reschedule244–252 ↗
fn pending_reflow_target_prevents_repeated_reschedule()

作用:测试某个目标宽度已经排队等待重排时,不要重复安排同一个宽度。

数据流:进去的是默认状态 → 观察 80 后确认 100 需要重排,再把 100 安排成待重排目标 → 出来断言 100 不再被认为需要新任务。

调用关系:它验证 schedule_debounced 和 reflow_needed_for_width 的配合,防止绘制循环不停给同一件事排队。

调用图:外部调用 2 个(assert!, default)。

tests::clear_pending_reflow_allows_same_width_to_be_rescheduled255–263 ↗
fn clear_pending_reflow_allows_same_width_to_be_rescheduled()

作用:测试清掉等待任务后,同一个宽度可以再次被判断为需要重排。

数据流:进去的是默认状态 → 安排 100 列的待重排任务,再调用 clear_pending_reflow → 出来断言 100 又变成需要重排。

调用关系:它验证 clear_pending_reflow 的效果。真正执行或取消任务后,状态机不能继续假装这个宽度已经在队列里。

调用图:外部调用 2 个(assert!, default)。

tests::mark_reflowed_width_reports_unchanged_width266–272 ↗
fn mark_reflowed_width_reports_unchanged_width()

作用:测试 mark_reflowed_width 能区分第一次记录和重复记录同一宽度。

数据流:进去的是默认状态 → 第一次标记 100 返回 true,第二次再标记 100 返回 false,并确认保存的宽度仍是 100 → 出来证明它能报告是否真的发生变化。

调用关系:它验证 mark_reflowed_width 的返回值语义。上层可以用这个返回值判断是否发生了新的有效修复。

调用图:外部调用 3 个(assert!, assert_eq!, default)。

tests::take_stream_finish_reflow_needed_drains_resize_request275–281 ↗
fn take_stream_finish_reflow_needed_drains_resize_request()

作用:测试 resize 发生在流式输出期间时,流结束检查会返回一次需要补修,然后就不再重复返回。

数据流:进去的是默认状态 → 先标记 resize_requested_during_stream,再连续调用 take_stream_finish_reflow_needed 两次 → 出来第一次为 true,第二次为 false。

调用关系:它验证 take_stream_finish_reflow_needed 是“取走式”的。这样一次流式输出结束只会触发一次最终重排。

调用图:外部调用 2 个(assert!, default)。

tests::take_stream_finish_reflow_needed_drains_ran_during_stream284–290 ↗
fn take_stream_finish_reflow_needed_drains_ran_during_stream()

作用:测试如果重排本身发生在流式输出期间,流结束后也会要求补一次正式重排,并且只要求一次。

数据流:进去的是默认状态 → 先标记 ran_during_stream,再连续读取是否需要流结束补修 → 出来第一次 true、第二次 false。

调用关系:它验证 mark_ran_during_stream 和 take_stream_finish_reflow_needed 的配合,防止临时流式内容的换行被永久保留下来。

调用图:外部调用 2 个(assert!, default)。

tests::clear_resets_stream_reflow_flags293–301 ↗
fn clear_resets_stream_reflow_flags()

作用:测试 clear 会连同流式输出相关的补修标记一起清掉。

数据流:进去的是默认状态 → 先设置两个流式输出标记,再调用 clear → 出来断言流结束检查不再认为需要重排。

调用关系:它验证 clear 是彻底重置。这个测试防止旧 transcript 被丢掉后,残留标记又让新内容莫名其妙重排。

调用图:外部调用 2 个(assert!, default)。

tui/src/app/resize_reflow.rs源码 ↗
orchestrationmain loop / resize handling / transcript replay

这个文件把终端 resize(窗口大小变化)和聊天记录重建连起来。程序真正保存的历史不是终端里那几行字,而是一组 HistoryCell(可以理解成“原始消息卡片”)。窗口宽度变了,它就用这些原始卡片重新生成适合新宽度的显示行,先清掉程序自己写进终端的旧滚动历史,再写入新版。它还特别照顾正在流式输出的消息:模型边生成边显示时,内容先是临时形态,结束后才合并成正式消息。如果 resize 正好发生在这个阶段,它会记一笔,等合并完再补一次重排,避免终端里留下临时换行。它也支持行数上限,只保留最新的若干行,像终端滚动缓冲区一样丢掉最旧的显示行。

函数细节24
trailing_run_start47–66 ↗
fn trailing_run_start(transcript_cells: &[Arc<dyn HistoryCell>]) -> usize

作用:这个函数从聊天记录尾部往前找,找出最后一串同类型、同一段流式消息相关的卡片从哪里开始。它用来判断末尾是不是还有没正式合并的流式内容。

数据流:进去的是一整串 transcript_cells,也就是保存聊天历史的原始卡片列表,以及要检查的卡片类型 T。它从最后一个卡片往前看,先跳过连续的“流式续段”,再把这一串的第一个非续段也包括进去。出来的是这串尾部内容的起始下标;如果尾部不是这种类型,就返回列表长度,表示没找到。

调用关系:它被 App::should_mark_reflow_as_stream_time 用来判断当前 resize 是否发生在流式消息尚未完全落定的阶段。这样后续重排可以知道,等流式消息合并后可能还要再修一次终端显示。

App::reset_history_emission_state69–72 ↗
fn reset_history_emission_state(&mut self)

作用:这个函数把“已经往历史里写过内容”的标记清空,并丢掉暂存的历史显示行。它通常在要重新开始写滚动历史时使用。

数据流:进去的是 App 当前状态。它把 has_emitted_history_lines 设回 false,并清空 deferred_history_lines。出来没有返回值,但 App 内部会像刚开始写历史一样重新计数和分隔。

调用关系:它会在 App::reflow_transcript_now、App::rebuild_transcript_after_backtrack 和 App::maybe_clear_resize_reflow_without_terminal 中使用。也就是说,只要终端历史要重建、撤回后要清理,或没有内容可重排时,都要先把这部分状态归零。

调用图:被 3 处调用(maybe_clear_resize_reflow_without_terminal, rebuild_transcript_after_backtrack, reflow_transcript_now)。

App::display_lines_for_history_insert74–89 ↗
fn display_lines_for_history_insert(
        &mut self,
        cell: &dyn HistoryCell,
        width: u16,
    ) -> Vec<HyperlinkLine>

作用:这个函数把一个历史卡片变成真正能写进终端的显示行,并在不同消息块之间自动加空行。它让聊天记录看起来不是挤成一团。

数据流:进去的是一个 HistoryCell 和目标宽度。它先让卡片按当前渲染模式生成带超链接信息的显示行;如果这不是流式续段,并且前面已经写过历史,就在开头插入一个空行当分隔。出来的是一组 HyperlinkLine,也就是终端可以显示、并可能带链接的文本行,同时可能更新 has_emitted_history_lines。

调用关系:它是插入单个历史卡片前的统一排版入口,被 App::insert_history_cell_lines 和 App::insert_history_cell_lines_with_initial_replay_buffer 调用。后面的函数再决定这些行是马上写进终端,还是先缓存起来。

调用图:调用 3 个内部函数(display_hyperlink_lines_for_mode, is_stream_continuation, new);被 2 处调用(insert_history_cell_lines, insert_history_cell_lines_with_initial_replay_buffer);外部调用 1 个(from)。

App::insert_history_cell_lines91–109 ↗
fn insert_history_cell_lines(
        &mut self,
        tui: &mut tui::Tui,
        cell: &dyn HistoryCell,
        width: u16,
    )

作用:这个函数把一个历史卡片的显示行写入终端滚动历史。遇到界面上有覆盖层时,它不会硬写,而是先暂存,避免和覆盖层抢画面。

数据流:进去的是 Tui、一个 HistoryCell 和宽度。它先调用 App::display_lines_for_history_insert 得到显示行;如果没有内容就结束;如果 overlay 存在,就把这些行放进 deferred_history_lines;否则按 App::history_line_wrap_policy 选定的换行方式写入终端历史。出来没有返回值,但终端或 App 的暂存队列会发生变化。

调用关系:它是普通历史插入路径的一部分。它把“卡片转显示行”的工作交给 App::display_lines_for_history_insert,把“真正写终端”的工作交给 Tui 的插入函数。

调用图:调用 2 个内部函数(display_lines_for_history_insert, history_line_wrap_policy);外部调用 1 个(insert_history_hyperlink_lines_with_wrap_policy)。

App::begin_initial_history_replay_buffer117–121 ↗
fn begin_initial_history_replay_buffer(&mut self)

作用:这个函数在恢复旧会话时开启一个缓冲区,先接住即将重放的大量历史行。这样启动时也能遵守和 resize 重排一样的最大保留行数。

数据流:进去的是 App 当前状态。它检查当前没有 overlay,才创建默认的 initial_history_replay_buffer。出来没有返回值,但 App 会进入“先缓存历史显示行”的状态。

调用关系:它通常在初始恢复历史开始前被调用。之后 App::insert_history_cell_lines_with_initial_replay_buffer 会把行放进这个缓冲区,最后由 App::finish_initial_history_replay_buffer 一次性刷到终端。

调用图:外部调用 1 个(default)。

App::begin_thread_switch_history_replay_buffer128–135 ↗
fn begin_thread_switch_history_replay_buffer(&mut self)

作用:这个函数在线程或会话切换时开启一种特殊缓冲:不逐条渲染旧历史,而是等全部源数据准备好后,只从尾部渲染终端会保留的那部分。它用来避免大历史切换时做无用功。

数据流:进去的是 App 当前状态。它先看是否设置了 resize 重排行数上限,并确认没有 overlay;满足条件时,创建 InitialHistoryReplayBuffer,标记 render_from_transcript_tail 为 true。出来没有返回值,但后续历史插入会被跳过,改为结束时从 transcript_cells 尾部统一渲染。

调用关系:它依赖 App::resize_reflow_max_rows 判断是否值得启用这个优化。结束时 App::finish_initial_history_replay_buffer 会看到这个标记,然后调用 App::render_transcript_lines_for_reflow 从最终历史尾部生成显示行。

调用图:调用 1 个内部函数(resize_reflow_max_rows);外部调用 1 个(new)。

App::finish_initial_history_replay_buffer142–166 ↗
fn finish_initial_history_replay_buffer(&mut self, tui: &mut tui::Tui)

作用:这个函数结束初始历史重放缓冲,并把最终该显示的历史行写入终端。它相当于“攒够了,现在统一倒进滚动历史”。

数据流:进去的是 Tui 和 App 中可能存在的 initial_history_replay_buffer。它先取走缓冲区;如果缓冲区已有保留行,就把这些行写进终端;如果没有行但标记了从 transcript 尾部渲染,就按当前终端宽度调用 App::render_transcript_lines_for_reflow 生成尾部行再写入。出来没有返回值,但终端滚动历史会被填入最终保留的内容。

调用关系:它是 App::begin_initial_history_replay_buffer 和 App::begin_thread_switch_history_replay_buffer 的收尾步骤。它会使用 App::history_line_wrap_policy 决定写入时的换行规则,并把实际写入交给 Tui。

调用图:调用 2 个内部函数(history_line_wrap_policy, render_transcript_lines_for_reflow);外部调用 1 个(insert_history_hyperlink_lines_with_wrap_policy)。

App::insert_history_cell_lines_with_initial_replay_buffer168–201 ↗
fn insert_history_cell_lines_with_initial_replay_buffer(
        &mut self,
        tui: &mut tui::Tui,
        cell: &dyn HistoryCell,
        width: u16,
    )

作用:这个函数是在“初始恢复历史”期间插入历史卡片的专用入口。它会根据是否启用了缓冲,决定是缓存、直接写入,还是完全跳过等待最后统一渲染。

数据流:进去的是 Tui、一个 HistoryCell 和宽度。若当前缓冲要求从 transcript 尾部统一渲染,它直接返回;否则先生成显示行。若有行数上限且缓冲存在,就用 App::buffer_initial_history_replay_display_lines 只保留最新行;若没有上限,则根据 overlay 情况选择暂存或直接写终端。出来没有返回值,但可能更新缓冲区、deferred_history_lines 或终端滚动历史。

调用关系:它连接了初始历史重放、行数上限和终端写入三件事。它调用 App::display_lines_for_history_insert 生成显示行,调用 App::resize_reflow_max_rows 看是否需要裁剪,并使用 App::history_line_wrap_policy 保持写入规则一致。

调用图:调用 3 个内部函数(display_lines_for_history_insert, history_line_wrap_policy, resize_reflow_max_rows);外部调用 2 个(buffer_initial_history_replay_display_lines, insert_history_hyperlink_lines_with_wrap_policy)。

App::history_line_wrap_policy203–209 ↗
fn history_line_wrap_policy(&self) -> HistoryLineWrapPolicy

作用:这个函数决定历史行写进终端时该怎么换行。简单说,普通模式下程序自己预先按聊天宽度折行,原始输出模式下交给终端自己折行。

数据流:进去的是 App 当前状态,主要读取 chat_widget.raw_output_mode。若处于 raw output mode,就返回 Terminal;否则返回 PreWrap。出来的是一个 HistoryLineWrapPolicy,供写入终端历史时使用。

调用关系:它被所有需要写入终端历史的路径复用,包括普通插入、初始重放结束、resize 重排和撤回后重建。这样不同场景下的换行行为不会互相打架。

调用图:被 5 处调用(finish_initial_history_replay_buffer, insert_history_cell_lines, insert_history_cell_lines_with_initial_replay_buffer, rebuild_transcript_after_backtrack, reflow_transcript_now)。

App::buffer_initial_history_replay_display_lines216–225 ↗
fn buffer_initial_history_replay_display_lines(
        buffer: &mut InitialHistoryReplayBuffer,
        display: Vec<HyperlinkLine>,
        max_rows: usize,
    )

作用:这个函数把初始恢复时生成的显示行放进缓冲区,并只保留最新的 max_rows 行。它模拟终端滚动历史“旧的先被挤掉”的行为。

数据流:进去的是缓冲区、一批显示行和最大行数。它把新行追加到 retained_lines 后面;如果总数超过 max_rows,就不断从前面弹掉最旧的行。出来没有返回值,但缓冲区只剩最新的一段显示内容。

调用关系:它被 App::insert_history_cell_lines_with_initial_replay_buffer 用在恢复大历史时。这样启动阶段写入的内容,和之后 resize 重排会保留的内容,是同一套规则。

App::schedule_resize_reflow227–229 ↗
fn schedule_resize_reflow(&mut self, target_width: Option<u16>) -> bool

作用:这个函数安排一次延迟的 resize 重排。延迟的意义是等用户拖动窗口稍微停一停,避免每抖一下宽度就立刻重建历史。

数据流:进去的是目标宽度,可能有值也可能没有。它把这个请求交给 transcript_reflow 的防抖调度器;出来是一个布尔值,表示这次调度是否需要马上请求一帧刷新。

调用关系:它被 App::handle_draw_size_change 调用。窗口大小变化时,App 不直接重排,而是先通过它排队,后面由 App::maybe_run_resize_reflow 在合适时机真正执行。

调用图:被 1 处调用(handle_draw_size_change)。

App::resize_reflow_max_rows231–233 ↗
fn resize_reflow_max_rows(&self) -> Option<usize>

作用:这个函数读取配置,算出 resize 重排最多应该保留多少终端行。它让大聊天记录不会在重排时无限制地全部写回终端。

数据流:进去的是 App 当前配置里的 terminal_resize_reflow。它调用 resize_reflow_cap 模块里的函数,把配置值转换成 Option<usize>:有些配置表示有上限,有些表示没有上限。出来的是最大行数或 None。

调用关系:它被 App::begin_thread_switch_history_replay_buffer、App::insert_history_cell_lines_with_initial_replay_buffer 和 App::render_transcript_lines_for_reflow 使用。也就是说,启动恢复、线程切换和窗口重排都遵守同一个行数上限。

调用图:调用 1 个内部函数(resize_reflow_max_rows);被 3 处调用(begin_thread_switch_history_replay_buffer, insert_history_cell_lines_with_initial_replay_buffer, render_transcript_lines_for_reflow)。

App::clear_terminal_for_resize_replay235–247 ↗
fn clear_terminal_for_resize_replay(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数在重写聊天历史前清空终端里旧的程序输出。它避免新排版的历史和旧排版的历史混在一起。

数据流:进去的是 Tui。它先判断是否在 alternate screen(备用屏幕,一些终端程序使用的独立画面)里:如果是,只清可见屏幕;如果不是,就清掉滚动历史和可见屏幕。随后如果 viewport 的 y 偏移不是 0,就把它拉回顶部。出来是成功或错误结果,终端显示区域会被清理。

调用关系:它被 App::reflow_transcript_now 和 App::rebuild_transcript_after_backtrack 调用。也就是 resize 重排和撤回后重建都会先清场,再把正确历史写回去。

调用图:被 2 处调用(rebuild_transcript_after_backtrack, reflow_transcript_now);外部调用 1 个(is_alt_screen_active)。

App::maybe_finish_stream_reflow256–264 ↗
fn maybe_finish_stream_reflow(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数在流式消息合并结束后,检查之前是否因为 resize 留下了必须补救的重排。如果需要,它马上补一次,确保终端显示的是正式消息的排版。

数据流:进去的是 Tui 和 App 当前的 transcript_reflow 状态。它先取出“流结束后需要重排”的标记;如果有,就调用 App::schedule_immediate_resize_reflow 并立刻尝试 App::maybe_run_resize_reflow;如果没有,但已有重排到期,就请求下一帧刷新。出来是成功或错误结果,可能触发一次立即重排或帧调度。

调用关系:它通常在 agent 消息流结束并完成合并后运行。它把紧急调度交给 App::schedule_immediate_resize_reflow,把真正执行交给 App::maybe_run_resize_reflow。

调用图:调用 2 个内部函数(maybe_run_resize_reflow, schedule_immediate_resize_reflow);外部调用 2 个(now, frame_requester)。

App::schedule_immediate_resize_reflow266–269 ↗
fn schedule_immediate_resize_reflow(&mut self, tui: &mut tui::Tui)

作用:这个函数安排一次“不要再等,马上做”的 resize 重排,并请求界面刷新一帧。它用于必须立刻修正终端历史的场景。

数据流:进去的是 Tui 和 App。它把 transcript_reflow 标记为立即执行,然后通过 frame_requester 请求一帧。出来没有返回值,但重排任务会变成待执行状态,界面也会被唤醒。

调用关系:它被 App::maybe_finish_stream_reflow 和 App::finish_required_stream_reflow 调用。之后 App::maybe_run_resize_reflow 会在这一帧或紧接着的流程里真正重建历史。

调用图:被 2 处调用(finish_required_stream_reflow, maybe_finish_stream_reflow);外部调用 1 个(frame_requester)。

App::finish_required_stream_reflow276–283 ↗
fn finish_required_stream_reflow(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数强制完成一次流式内容结束后的重排。它比普通检查更坚决,适合那些如果不重排就一定会留下错误换行的内容,比如计划消息合并。

数据流:进去的是 Tui。它先调用 App::schedule_immediate_resize_reflow 安排立即重排,再调用 App::maybe_run_resize_reflow 执行;如果执行后已经没有待处理重排,就清掉流式相关标记。出来是成功或错误结果,终端历史通常会被修正为正式内容的排版。

调用关系:它用于必须经过 resize 重排路径来修复显示的流式合并场景。它依赖 App::schedule_immediate_resize_reflow 唤起任务,再依赖 App::maybe_run_resize_reflow 做实际重建。

调用图:调用 2 个内部函数(maybe_run_resize_reflow, schedule_immediate_resize_reflow)。

App::handle_draw_size_change291–320 ↗
fn handle_draw_size_change(
        &mut self,
        size: ratatui::layout::Size,
        last_known_screen_size: ratatui::layout::Size,
        frame_requester: &tui::FrameRequester,
    ) -> bool

作用:这个函数在每次绘制前检查终端大小有没有变化,并决定是否要重建聊天历史。它是 resize 事件进入重排系统的主要入口。

数据流:进去的是当前终端大小、上次记录的大小和帧请求器。它记录新宽度,判断宽度变化是否影响换行,也判断高度变化是否影响可见区域;需要重建时,它可能标记“这次 resize 发生在流式输出期间”,然后调用 App::schedule_resize_reflow 安排重排,并请求现在或稍后刷新。它还会在大小真的变了时刷新状态栏。出来是布尔值,表示是否应该重建 transcript。

调用关系:它由 App::handle_draw_pre_render 调用,是每帧绘制前的尺寸检查步骤。它会调用 App::should_mark_reflow_as_stream_time 判断流式状态,调用 App::maybe_clear_resize_reflow_without_terminal 清理无意义的 pending 任务。

调用图:调用 5 个内部函数(maybe_clear_resize_reflow_without_terminal, schedule_resize_reflow, should_mark_reflow_as_stream_time, schedule_frame, schedule_frame_in);被 1 处调用(handle_draw_pre_render)。

App::maybe_clear_resize_reflow_without_terminal322–333 ↗
fn maybe_clear_resize_reflow_without_terminal(&mut self)

作用:这个函数清理一种空转情况:已经排了 resize 重排,但现在没有 overlay,也没有聊天历史可重建。它避免系统一直挂着一个没必要的重排任务。

数据流:进去的是 App 当前状态。它先看是否有 pending deadline;如果还没到期、有 overlay、或 transcript_cells 不为空,就什么也不做。只有在到期、无覆盖层、无历史卡片时,它才清掉 pending reflow,并调用 App::reset_history_emission_state。出来没有返回值,但重排状态可能被清空。

调用关系:它被 App::handle_draw_size_change 在尺寸检查末尾调用。作用是给 resize 调度系统做扫尾,防止没有终端历史时还一直等着重排。

调用图:调用 1 个内部函数(reset_history_emission_state);被 1 处调用(handle_draw_size_change);外部调用 1 个(now)。

App::handle_draw_pre_render335–350 ↗
fn handle_draw_pre_render(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数在真正绘制界面之前运行,先处理终端大小变化,再尝试执行到期的重排。它像每帧开始前的“整理桌面”。

数据流:进去的是 Tui。它读取当前终端大小,把当前大小和上次大小交给 App::handle_draw_size_change;如果需要重建历史,就先清掉 Tui 里排队但还没写入的旧宽度历史行;然后调用 App::maybe_run_resize_reflow。出来是成功或错误结果,可能完成一次历史重排。

调用关系:它是绘制周期中的前置步骤。尺寸判断交给 App::handle_draw_size_change,实际重排交给 App::maybe_run_resize_reflow,中间负责丢掉已经不可信的待写历史行。

调用图:调用 2 个内部函数(handle_draw_size_change, maybe_run_resize_reflow);外部调用 2 个(clear_pending_history_lines, frame_requester)。

App::maybe_run_resize_reflow358–396 ↗
fn maybe_run_resize_reflow(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数检查 resize 重排是否已经到点,如果到点且没有覆盖层,就真正执行重排。它负责把“已调度的任务”变成“实际清屏并重写历史”。

数据流:进去的是 Tui 和 App 的 pending reflow 状态。它先看有没有 deadline;没到点就重新安排稍后的刷新;有 overlay 就暂缓;到点后清掉 pending 标记,判断这次是否发生在流式期间,然后调用 App::reflow_transcript_now 重建终端历史,并记录已经按哪个终端宽度重排过。最后它还安排一次便宜的后续刷新,用来捕捉终端最终稳定的宽度。出来是成功或错误结果。

调用关系:它被 App::handle_draw_pre_render、App::maybe_finish_stream_reflow 和 App::finish_required_stream_reflow 调用。真正的重建工作由 App::reflow_transcript_now 完成;流式判断则交给 App::should_mark_reflow_as_stream_time。

调用图:调用 2 个内部函数(reflow_transcript_now, should_mark_reflow_as_stream_time);被 3 处调用(finish_required_stream_reflow, handle_draw_pre_render, maybe_finish_stream_reflow);外部调用 2 个(now, frame_requester)。

App::reflow_transcript_now398–424 ↗
fn reflow_transcript_now(&mut self, tui: &mut tui::Tui) -> Result<u16>

作用:这个函数立刻按照当前终端宽度重建聊天滚动历史。它是 resize 重排真正“动手清掉旧内容、写入新内容”的地方。

数据流:进去的是 Tui。它读取终端宽度,再通过 chat_widget 算出历史内容实际可用的换行宽度;如果没有历史卡片,就清掉待写历史行并重置发射状态后返回。否则它调用 App::render_transcript_lines_for_reflow 生成新显示行,清掉待写行,调用 App::clear_terminal_for_resize_replay 清终端,再把新行按 App::history_line_wrap_policy 写入终端。出来是本次观察到的终端宽度。

调用关系:它只由 App::maybe_run_resize_reflow 调用,是已调度 resize 任务的执行核心。它把生成显示行交给 App::render_transcript_lines_for_reflow,把清屏交给 App::clear_terminal_for_resize_replay。

调用图:调用 4 个内部函数(clear_terminal_for_resize_replay, history_line_wrap_policy, render_transcript_lines_for_reflow, reset_history_emission_state);被 1 处调用(maybe_run_resize_reflow);外部调用 2 个(clear_pending_history_lines, insert_history_hyperlink_lines_with_wrap_policy)。

App::rebuild_transcript_after_backtrack431–453 ↗
fn rebuild_transcript_after_backtrack(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:这个函数在撤回或回退聊天内容后,按剩下的历史重新写终端。即使没有历史了,它也必须清终端,避免被撤回的内容还留在滚动区里。

数据流:进去的是 Tui。它读取当前终端宽度并算出历史换行宽度;如果 transcript_cells 为空,就重置发射状态并准备空列表;否则调用 App::render_transcript_lines_for_reflow 生成剩余历史行。然后它清掉待写历史行,调用 App::clear_terminal_for_resize_replay 清终端,清空 deferred_history_lines,最后把剩余行写回终端。出来是成功或错误结果。

调用关系:它是 rollback/backtrack 场景的专用重建路径,和 resize 重排很像,但更严格:没有历史也要清屏。它复用 App::render_transcript_lines_for_reflow、App::clear_terminal_for_resize_replay 和 App::history_line_wrap_policy。

调用图:调用 4 个内部函数(clear_terminal_for_resize_replay, history_line_wrap_policy, render_transcript_lines_for_reflow, reset_history_emission_state);外部调用 3 个(new, clear_pending_history_lines, insert_history_hyperlink_lines_with_wrap_policy)。

App::render_transcript_lines_for_reflow462–523 ↗
fn render_transcript_lines_for_reflow(&mut self, width: u16) -> ReflowRenderResult

作用:这个函数把保存的原始历史卡片重新渲染成适合当前宽度的终端行。它不直接写终端,只产出“应该写哪些行”。

数据流:进去的是目标显示宽度。它先读取行数上限;如果有限制,就从 transcript_cells 尾部往前渲染,够用后停止,避免把全部旧历史都格式化。若保留范围正好切进一串流式续段,它会继续往前补上这一串的开头,避免分隔空行插错。之后它在不同非续段消息之间补空行,最后如果行数仍超过上限,就从前面裁掉旧行。出来是 ReflowRenderResult,里面装着最终要写入终端的 HyperlinkLine,同时会更新 has_emitted_history_lines。

调用关系:它被 App::reflow_transcript_now、App::rebuild_transcript_after_backtrack 和 App::finish_initial_history_replay_buffer 调用。它是所有“从原始历史重建显示行”路径共享的渲染核心。

调用图:调用 2 个内部函数(resize_reflow_max_rows, new);被 3 处调用(finish_initial_history_replay_buffer, rebuild_transcript_after_backtrack, reflow_transcript_now);外部调用 3 个(from, new, new)。

App::should_mark_reflow_as_stream_time530–537 ↗
fn should_mark_reflow_as_stream_time(&self) -> bool

作用:这个函数判断当前是否应该把 resize 重排视为“发生在流式输出期间”。这个判断很重要,因为流式临时内容结束后可能需要再做一次正式重排。

数据流:进去的是 App 当前状态。它检查 chat_widget 里是否有活跃的 agent stream 或 plan stream;还检查 transcript_cells 末尾是否有 AgentMessageCell 或 ProposedPlanStreamCell 这类尚未合并的流式尾段。出来是 true 或 false。

调用关系:它被 App::handle_draw_size_change 和 App::maybe_run_resize_reflow 使用。前者用它标记 resize 请求发生在流式期间,后者用它标记实际重排发生在流式期间,方便合并结束后补救。

调用图:被 2 处调用(handle_draw_size_change, maybe_run_resize_reflow)。

执行与辅助转录渲染

这些文件涵盖执行单元模型与渲染,以及用于 diff、指令、覆盖层和紧凑状态/页脚界面的相邻丰富渲染器。

tui/src/exec_cell/mod.rs源码 ↗
orchestrationcross-cutting

这个文件像一个小目录页,告诉程序:和“执行命令的显示格子”有关的东西,分别放在 model 和 render 两个子文件里。model 里主要是数据长什么样,比如一条命令输出、一个执行单元;render 里主要是怎么把这些内容画到终端界面上。这里用 pub(crate) use 把常用名字重新导出,意思是:同一个 crate,也就是同一个 Rust 包内部的其他代码,可以直接从 exec_cell 这个模块拿到这些工具,不必知道它们具体藏在哪个子文件里。这样做的好处是减少耦合:以后内部文件怎么拆、怎么改,外面的调用方式可以尽量不变。还有一个测试专用的 ExecCall,只在测试时开放,避免正式代码误用测试辅助结构。

tui/src/exec_cell/model.rs源码 ↗
data_modelmain loop / command execution display

用户在终端界面里让系统执行命令时,界面不能只显示一堆散乱文字,它需要知道:这是哪个命令、跑完没有、输出了什么、花了多久、是不是用户自己敲的 shell 命令。这个文件就像给“命令执行记录”做了一个小档案夹。ExecCall 表示一次具体命令调用,ExecCell 表示界面上的一个记录格子,里面可以只有一个命令,也可以把一组类似“读文件、列目录、搜索”的探索类命令合并显示。这里特别重视 call_id,也就是每次命令的身份证号;如果结束事件找不到对应身份证号,就不能硬塞进别的格子,否则聊天记录会被错误合并。文件还处理运行中追加输出、完成命令、失败收尾、判断是否还活跃、是否允许动画等状态。

函数细节14
ExecCell::new42–47 ↗
fn new(call: ExecCall, animations_enabled: bool) -> Self

作用:创建一个新的命令记录格子,里面先放入一条命令调用。有人要在聊天记录里开始显示一个新命令时,会用它。

数据流:进去的是一条 ExecCall 命令记录和一个“是否开启动画”的开关;函数把这条命令放进一个列表里,再把动画开关一起保存;出来的是一个新的 ExecCell,可以被界面继续更新和显示。

调用关系:它是命令格子的起点。TUI 在新命令开始时,以及不少显示测试里都会用它先搭出一个记录格子,后续再由追加输出、完成命令等函数继续改变这个格子的状态。

调用图:被 17 处调用(new_active_exec_command, active_command_without_animations_is_stable, command_display_does_not_split_long_url_token, desired_transcript_height_accounts_for_wrapped_url_like_rows, exploring_display_does_not_split_long_url_like_search_query, output_display_does_not_split_long_url_like_token_without_scheme, user_shell_output_is_limited_by_screen_lines, coalesced_reads_dedupe_names, coalesces_reads_across_multiple_calls, coalesces_sequential_reads_within_one_call (+7 more));外部调用 1 个(vec!)。

ExecCell::with_added_call49–75 ↗
fn with_added_call(
        &self,
        call_id: String,
        command: Vec<String>,
        parsed: Vec<ParsedCommand>,
        source: ExecCommandSource,
        interaction_input: Option<Strin

作用:尝试把一条新的命令追加到已有格子里。它只允许把同类的“探索命令”,比如读文件、列目录、搜索,合并到同一个格子里。

数据流:进去的是新命令的编号、原始命令、解析后的命令类型、来源和交互输入;函数先做出一条新的 ExecCall,并记录当前开始时间;然后检查旧格子和新命令是不是都属于探索类;如果是,就返回一个包含新旧所有命令的新格子,如果不是,就返回空,表示不能合并。

调用关系:它接在已有 ExecCell 之后工作,像是在问“这张档案夹还能不能继续塞同类文件”。它会调用 ExecCell::is_exploring_cell 判断旧格子,再调用 ExecCell::is_exploring_call 判断新命令,避免把普通 shell 命令错误合并进探索记录。

调用图:调用 1 个内部函数(is_exploring_cell);外部调用 3 个(now, is_exploring_call, vec!)。

ExecCell::complete_call82–95 ↗
fn complete_call(
        &mut self,
        call_id: &str,
        output: CommandOutput,
        duration: Duration,
    ) -> bool

作用:把某个命令标记为已经结束,并填上最终输出和耗时。它还能告诉调用者:到底有没有找到这条命令。

数据流:进去的是命令编号、最终输出和运行时长;函数从后往前找编号相同的命令,因为最近的同名调用最可能是目标;找到后写入输出、写入耗时、清掉开始时间;出来的是 true。找不到就什么都不改,返回 false。

调用关系:它通常在收到“命令结束”事件时使用。返回 false 很重要,因为这说明事件没有匹配到当前格子,外层聊天界面就应该把它当作单独的孤儿结束事件处理,而不是乱贴到正在显示的探索格子里。

ExecCell::should_flush97–99 ↗
fn should_flush(&self) -> bool

作用:判断这个格子是否应该从“正在更新”变成固定历史记录。普通单命令跑完后通常可以刷入历史,探索类合并格子则不按这个规则立即刷走。

数据流:进去的是当前 ExecCell 自己的状态;函数先看它是不是探索类格子,再看里面每条命令是否都有输出;如果不是探索类,并且所有命令都完成了,就返回 true,否则返回 false。

调用关系:它被外层显示流程用来决定何时把活动命令记录固定下来。它会调用 ExecCell::is_exploring_cell,因为探索类命令需要继续合并展示,不能简单按“完成就冲走”的普通规则处理。

调用图:调用 1 个内部函数(is_exploring_cell)。

ExecCell::mark_failed101–117 ↗
fn mark_failed(&mut self)

作用:把这个格子里还没结束的命令统一标记成失败。比如程序中断或执行流程出错时,它能给未完成命令一个明确结局。

数据流:进去的是一个可修改的 ExecCell;函数逐条查看命令,已经有输出的跳过,没输出的就计算从开始到现在过了多久,没有开始时间就按 0 毫秒算;然后清掉开始时间,填入失败耗时,并放入一个退出码为 1、输出为空的失败结果。

调用关系:它像清场用的应急按钮。外层流程如果发现命令不会再正常结束,就可以调用它,避免界面上永远留着一个看起来还在跑的命令。

调用图:外部调用 1 个(new)。

ExecCell::is_exploring_cell119–121 ↗
fn is_exploring_cell(&self) -> bool

作用:判断这个格子是不是完全由探索类命令组成。探索类命令指的是系统为了查看环境而做的读文件、列目录、搜索等动作。

数据流:进去的是当前格子里的命令列表;函数逐条交给 ExecCell::is_exploring_call 判断;只有所有命令都是探索类时,才返回 true。

调用关系:它是合并和刷新规则的基础判断。ExecCell::with_added_call 用它决定能不能继续合并新命令,ExecCell::should_flush 用它决定普通完成规则是否适用。

调用图:被 2 处调用(should_flush, with_added_call)。

ExecCell::is_active123–125 ↗
fn is_active(&self) -> bool

作用:判断这个格子里是否还有命令没跑完。界面可以用它决定是否显示“进行中”的状态。

数据流:进去的是当前格子;函数查看每条命令有没有最终输出;只要有一条还没有输出,就返回 true,全部都有输出才返回 false。

调用关系:它通常服务于显示层。外层界面想知道某个命令格子是不是还要继续刷新、显示动画或等待结果时,会查看这个状态。

ExecCell::active_start_time127–132 ↗
fn active_start_time(&self) -> Option<Instant>

作用:找出第一个还在运行的命令是什么时候开始的。这样界面可以显示已经运行了多久。

数据流:进去的是当前格子;函数找到第一条没有最终输出的命令,再取出它的开始时间;如果没有运行中的命令,或者没有记录开始时间,就返回空。

调用关系:它给计时显示提供原始时间点。外层界面拿到这个时间后,可以算出“已运行几秒”之类的信息。

ExecCell::animations_enabled134–136 ↗
fn animations_enabled(&self) -> bool

作用:告诉外界这个命令格子是否允许显示动画。比如运行中的转圈或闪动效果,可以由这个开关控制。

数据流:进去的是当前格子;函数直接读出保存好的 animations_enabled 值;出来的是 true 或 false,不修改任何状态。

调用关系:它是显示层的一个小开关。ExecCell::new 创建格子时保存这个设置,之后界面渲染时读取它,决定要不要用动态效果。

ExecCell::iter_calls138–140 ↗
fn iter_calls(&self) -> impl Iterator<Item = &ExecCall>

作用:让外界按顺序查看这个格子里的所有命令调用。它适合用在渲染界面或统计内容时。

数据流:进去的是当前格子;函数不复制命令,只给出一个可以逐个查看 ExecCall 的迭代器,也就是“按顺序翻阅列表”的工具;它不改动格子。

调用关系:它是 ExecCell 对外展示内部命令列表的安全入口。渲染代码可以通过它读每条命令的信息,而不需要直接拿走或修改内部列表。

ExecCell::append_output142–152 ↗
fn append_output(&mut self, call_id: &str, chunk: &str) -> bool

作用:给正在执行的某个命令追加一段新输出。命令运行时经常不是一次性吐完结果,而是一小段一小段来,这个函数就是负责接这些片段。

数据流:进去的是命令编号和一段输出文字;如果文字是空的,函数直接返回 false;否则它从后往前找对应命令,找不到也返回 false;找到后,如果这条命令还没有输出档案,就先建一个默认档案,再把这段文字追加到 aggregated_output 里,最后返回 true。

调用关系:它通常在收到“命令输出流片段”时使用,位置早于最终完成事件。之后 ExecCell::complete_call 可能会写入最终结果和耗时;append_output 负责的是运行中不断累积屏幕上看到的内容。

ExecCell::is_exploring_call154–165 ↗
fn is_exploring_call(call: &ExecCall) -> bool

作用:判断一条具体命令是不是探索类命令。这里的探索类不是用户直接开的 shell,而是系统为了了解文件和内容而执行的读、列、搜。

数据流:进去的是一条 ExecCall;函数检查三个条件:来源不能是用户 shell,解析结果不能为空,并且每个解析出来的动作都必须是读文件、列文件或搜索;全部满足才返回 true。

调用关系:它是探索命令分组的核心小规则。ExecCell::with_added_call 和 ExecCell::is_exploring_cell 都依赖它,确保只有真正同类的查看动作才会被合并在一个界面格子里。

调用图:外部调用 1 个(matches!)。

ExecCall::is_user_shell_command169–171 ↗
fn is_user_shell_command(&self) -> bool

作用:判断这条命令是不是用户自己发起的 shell 命令。shell 可以理解成用户直接和系统命令行对话的入口。

数据流:进去的是一条 ExecCall;函数查看它的 source 来源字段;如果来源等于 UserShell 就返回 true,否则返回 false,不改任何内容。

调用关系:它给显示层或判断逻辑提供一个清晰问题:“这是用户亲手敲的命令吗?”有了这个判断,界面就能把用户命令和系统内部查看命令区别对待。

调用图:外部调用 1 个(matches!)。

ExecCall::is_unified_exec_interaction173–175 ↗
fn is_unified_exec_interaction(&self) -> bool

作用:判断这条命令是不是来自统一执行交互。也就是某种更整合的执行入口,而不是普通用户 shell。

数据流:进去的是一条 ExecCall;函数读取 source 来源字段;如果它是 UnifiedExecInteraction 就返回 true,否则返回 false,原数据不变。

调用关系:它是给其他界面或流程做分类用的小判断。外层代码可以根据这个结果,决定这条命令在聊天记录里该用哪种展示方式或交互方式。

调用图:外部调用 1 个(matches!)。

tui/src/exec_cell/render.rs源码 ↗
domain_logicmain loop / rendering

程序运行命令时,终端界面不能把原始数据一股脑倒出来,否则用户会被很长的日志、换行和颜色控制符淹没。这个文件就像一个“排版员”:先判断命令是在运行、成功还是失败,再给它配上圆点、标题和颜色;然后把命令本身做语法高亮,并按屏幕宽度换行;最后把输出内容缩短,只保留开头和结尾,中间用“省略了多少行”的提示代替,还告诉用户可以按 ctrl+t 看完整记录。它还区分两种场景:普通执行命令,以及“探索文件/搜索内容”这种更像浏览项目的行为。文件底部的测试重点保护几个容易出问题的地方,比如超长 URL 不要被乱切、很长输出要按屏幕实际行数限制、隐藏行数的提示要准确。

函数细节31
new_active_exec_command44–65 ↗
fn new_active_exec_command(
    call_id: String,
    command: Vec<String>,
    parsed: Vec<ParsedCommand>,
    source: ExecCommandSource,
    interaction_input: Option<String>,
    animations_enabled:

作用:创建一个“正在执行中的命令显示单元”。别人刚发起一条命令时会用它,把命令信息包装成界面可以渲染的 ExecCell。

数据流:输入命令编号、命令参数、解析后的命令含义、来源、可能的交互输入、是否开启动画 → 它记录当前时间作为开始时间,并把输出和耗时先留空 → 返回一个表示“命令正在跑”的 ExecCell。

调用关系:它是执行显示流程的起点之一,内部把数据交给 ExecCell::new,并用 Instant::now 记下开始时间;后续渲染时 ExecCell::display_lines 会根据这些信息显示运行状态。

调用图:调用 1 个内部函数(new);外部调用 1 个(now)。

format_unified_exec_interaction67–80 ↗
fn format_unified_exec_interaction(command: &[String], input: Option<&str>) -> String

作用:把一次“和正在运行的命令交互”的行为写成一句人能看懂的话。比如是等待某个命令,还是给它发送了输入。

数据流:输入原始命令数组和可选的输入文本 → 它尽量提取真正的 bash 脚本,否则把命令数组拼起来;如果有输入,就生成输入预览 → 输出一句类似“Interacted with ...”或“Waited for ...”的说明。

调用关系:ExecCell::command_display_lines 在发现这是统一的执行交互时会调用它;它自己会把输入预览交给 summarize_interaction_input 来缩短和转义。

调用图:调用 2 个内部函数(extract_bash_command, summarize_interaction_input);被 1 处调用(command_display_lines);外部调用 1 个(format!)。

summarize_interaction_input82–95 ↗
fn summarize_interaction_input(input: &str) -> String

作用:把用户发给命令的输入压缩成短预览,防止大段输入挤满界面。

数据流:输入一段文本 → 它把换行改成可见的 \n,把反引号转义,再限制最多 80 个字符 → 输出一段安全、短小的预览文字,太长时末尾加省略号。

调用关系:它只服务于 format_unified_exec_interaction;当界面需要展示“发送了什么输入”时,会先经过这里做清理和截断。

调用图:被 1 处调用(format_unified_exec_interaction);外部调用 1 个(new)。

output_lines103–184 ↗
fn output_lines(
    output: Option<&CommandOutput>,
    params: OutputLinesParams,
) -> OutputLines

作用:把命令输出整理成适合界面显示的行,并在输出太长时只留头尾。它避免几千行日志直接占满终端。

数据流:输入可选的 CommandOutput 和显示参数,比如最多保留多少行、是否只看错误、是否加缩进前缀 → 它读取 aggregated_output,按行拆开,给 ANSI 颜色控制符转成界面样式,必要时中间插入“省略了 N 行”的提示 → 输出整理后的 Line 列表和已省略行数。

调用关系:ExecCell::command_display_lines 用它生成命令输出区域;相关测试也直接调用它,确认省略提示里会带上查看完整记录的快捷键。

调用图:被 3 处调用(command_display_lines, output_lines_ellipsis_includes_transcript_hint, user_shell_output_is_limited_by_screen_lines);外部调用 3 个(new, ansi_escape_line, output_ellipsis_line)。

activity_marker186–193 ↗
fn activity_marker(start_time: Option<Instant>, animations_enabled: bool) -> Span<'static>

作用:生成运行中的小标记,比如动画点或静态圆点。它让用户一眼知道命令还没结束。

数据流:输入开始时间和是否开启动画 → 它根据动画设置选择活动指示器;如果拿不到动画帧,就用一个灰色圆点兜底 → 输出一个可渲染的 Span。

调用关系:ExecCell::command_display_lines 在命令还没出结果时调用它;它把具体动画选择交给 activity_indicator 和 MotionMode。

调用图:调用 2 个内部函数(from_animations_enabled, activity_indicator);被 1 处调用(command_display_lines)。

ExecCell::display_lines196–202 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:这是 ExecCell 对外提供的主要显示入口:给定屏幕宽度,返回应该画出来的文字行。

数据流:输入当前单元和终端宽度 → 它先判断这是“探索型单元”还是普通命令单元 → 输出对应的一组界面行。

调用关系:它实现 HistoryCell 接口,外层历史列表渲染时会调用它;它根据情况把工作分给 exploring_display_lines 或 command_display_lines。

调用图:调用 2 个内部函数(command_display_lines, exploring_display_lines)。

ExecCell::transcript_lines204–246 ↗
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成完整记录视图里的内容。和普通界面不同,它尽量展示完整命令和完整输出,方便用户回看。

数据流:输入当前单元和宽度 → 它逐个命令显示 shell 命令、格式化后的输出、成功或失败标记以及耗时;输出会按宽度换行 → 返回完整记录用的行列表。

调用关系:ExecCell::raw_lines 会基于它生成纯文本;普通界面里的省略提示也会提醒用户去 transcript,也就是这里生成的完整记录。

调用图:调用 6 个内部函数(strip_bash_lc_and_escape, highlight_bash_to_lines, push_owned_lines, new, adaptive_wrap_line, adaptive_wrap_lines);被 1 处调用(raw_lines);外部调用 3 个(from, format!, vec!)。

ExecCell::raw_lines248–250 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:把完整记录转成不带复杂样式的普通文字行。需要复制、保存或做纯文本处理时会用它。

数据流:输入当前单元 → 它先用 transcript_lines 生成超宽情况下的完整记录,再通过 plain_lines 去掉样式 → 输出纯文本形式的行。

调用关系:它也是 HistoryCell 接口的一部分;真正的记录内容来自 transcript_lines,样式清理交给 plain_lines。

调用图:调用 2 个内部函数(transcript_lines, plain_lines)。

ExecCell::output_ellipsis_text254–256 ↗
fn output_ellipsis_text(omitted: usize) -> String

作用:生成“输出中间省略了多少行”的提示文字,并附带查看完整记录的快捷键。

数据流:输入省略的行数 → 它把数字填进固定模板 → 输出类似“… +3 lines (ctrl + t to view transcript)”的字符串。

调用关系:output_lines、output_ellipsis_line_with_prefix 等地方会间接或直接依赖这段统一文案,保证界面里的省略提示一致。

调用图:外部调用 1 个(format!)。

ExecCell::output_ellipsis_line258–260 ↗
fn output_ellipsis_line(omitted: usize) -> Line<'static>

作用:把输出省略提示包装成一整行界面元素。

数据流:输入省略行数 → 它调用统一的省略文案并设置成灰色弱化显示 → 输出一个 Line。

调用关系:output_lines 在原始输出太长时调用它,把这行插到保留的开头和结尾之间。

调用图:外部调用 2 个(from, vec!)。

ExecCell::exploring_display_lines262–363 ↗
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:渲染“探索项目”类命令,比如读取文件、列目录、搜索内容。它把底层命令翻译成更像人话的“Read / List / Search”。

数据流:输入当前单元和屏幕宽度 → 它先显示 Exploring 或 Explored,再把连续读取操作合并,把解析后的命令变成简短动作说明,并按宽度换行加缩进 → 输出探索过程的显示行。

调用关系:ExecCell::display_lines 判断当前单元是探索型时会调用它;它依赖 prefix_lines、adaptive_wrap_line 和 push_owned_lines 完成缩进和换行。

调用图:调用 4 个内部函数(prefix_lines, push_owned_lines, new, adaptive_wrap_line);被 1 处调用(display_lines);外部调用 3 个(from, new, vec!)。

ExecCell::command_display_lines365–508 ↗
fn command_display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:渲染普通命令执行的主界面,包括标题、命令文本、运行状态和输出摘要。

数据流:输入当前单元和屏幕宽度 → 它判断成功、失败或运行中,生成标题;把命令做清理和高亮;把长命令换行并限制行数;如果有输出,再整理、换行、截断并加缩进 → 输出最终显示在历史里的行。

调用关系:ExecCell::display_lines 对普通命令会调用它;它串起 activity_marker、format_unified_exec_interaction、output_lines、highlight_bash_to_lines、truncate_lines_middle 等多个小工具,是本文件最核心的排版流程。

调用图:调用 9 个内部函数(activity_marker, format_unified_exec_interaction, output_lines, strip_bash_lc_and_escape, highlight_bash_to_lines, prefix_lines, push_owned_lines, new, adaptive_wrap_line);被 1 处调用(display_lines);外部调用 7 个(from, limit_lines_from_start, truncate_lines_middle, from, new, panic!, vec!)。

ExecCell::limit_lines_from_start510–521 ↗
fn limit_lines_from_start(lines: &[Line<'static>], keep: usize) -> Vec<Line<'static>>

作用:只保留一组行最前面的若干行,后面用简单省略号说明少了多少行。主要用于限制太长的命令续行。

数据流:输入行列表和要保留的数量 → 如果没超限就原样返回;如果超限就保留前 keep 行,并追加“… +N lines” → 输出缩短后的行列表。

调用关系:ExecCell::command_display_lines 用它限制命令本身的续行;它和输出截断不同,不带 transcript 快捷键,因为这里省略的是命令显示续行。

调用图:外部调用 4 个(len, to_vec, ellipsis_line, vec!)。

ExecCell::truncate_lines_middle539–630 ↗
fn truncate_lines_middle(
        lines: &[Line<'static>],
        max_rows: usize,
        width: u16,
        omitted_hint: Option<usize>,
        ellipsis_prefix: Option<Line<'static>>,
    ) -> Ve

作用:按屏幕实际占用的行数截断长输出,保留开头和结尾,中间放省略提示。它解决“一个很长的逻辑行换成几十屏幕行”的问题。

数据流:输入已排好前缀的行、最大屏幕行数、终端宽度、已有省略数量和可选前缀 → 它先计算每一行在当前宽度下会占几行,再决定头部和尾部各保留多少,最后插入带前缀的省略提示 → 输出不会超过预算太多的显示行。

调用关系:ExecCell::command_display_lines 在输出换行之后调用它;它会用 output_ellipsis_row_count 预估省略提示自身高度,并用 output_ellipsis_line_with_prefix 生成提示行。

调用图:外部调用 8 个(iter, len, to_vec, output_ellipsis_line_with_prefix, output_ellipsis_row_count, new, from, vec!)。

ExecCell::ellipsis_line632–634 ↗
fn ellipsis_line(omitted: usize) -> Line<'static>

作用:生成不带 transcript 提示的简单省略行。

数据流:输入省略行数 → 它生成“… +N lines”并设为灰色 → 输出一个 Line。

调用关系:ExecCell::limit_lines_from_start 用它表示命令续行被截掉了;这和输出区域的省略提示分开处理。

调用图:外部调用 2 个(from, vec!)。

ExecCell::output_ellipsis_row_count636–647 ↗
fn output_ellipsis_row_count(
        omitted: usize,
        width: u16,
        prefix: Option<&Line<'static>>,
    ) -> usize

作用:计算输出省略提示在当前终端宽度下会占几行。窄屏时一句提示可能会自动换成多行。

数据流:输入省略行数、宽度和可选前缀 → 它先构造省略提示行,再用 Paragraph 的换行计算得到实际屏幕行数 → 输出至少为 1 的行数。

调用关系:ExecCell::truncate_lines_middle 在截断前调用它,为省略提示预留空间,避免提示本身把显示预算撑爆。

调用图:外部调用 3 个(new, from, vec!)。

ExecCell::output_ellipsis_line_with_prefix651–658 ↗
fn output_ellipsis_line_with_prefix(
        omitted: usize,
        prefix: Option<&Line<'static>>,
    ) -> Line<'static>

作用:生成带缩进前缀的输出省略提示,让它和输出内容左边对齐。

数据流:输入省略行数和可选前缀 → 它复制前缀,再把统一省略文案追加进去 → 输出一行对齐好的提示。

调用关系:ExecCell::truncate_lines_middle 和 output_ellipsis_row_count 都会用它;省略文案来自 output_ellipsis_text。

调用图:外部调用 1 个(output_ellipsis_text)。

PrefixedBlock::new668–673 ↗
fn new(initial_prefix: &'static str, subsequent_prefix: &'static str) -> Self

作用:创建一个带“首行前缀”和“后续行前缀”的排版块配置。

数据流:输入首行前缀和后续行前缀 → 它保存这两个字符串 → 输出 PrefixedBlock 配置。

调用关系:文件底部的 EXEC_DISPLAY_LAYOUT 用它描述命令续行和输出块的缩进样式;后续 wrap_width 会根据这些前缀算剩余宽度。

PrefixedBlock::wrap_width675–679 ↗
fn wrap_width(self, total_width: u16) -> usize

作用:算出扣掉左侧前缀后,正文还剩多少宽度可以显示。

数据流:输入终端总宽度 → 它计算两个前缀里更宽的那个,占掉这部分宽度;剩下的宽度至少保留 1 → 输出正文换行用的宽度。

调用关系:ExecCell::command_display_lines 用它给命令续行和输出内容设置换行宽度,避免文字压到缩进符号上。

调用图:外部调用 2 个(width, from)。

ExecDisplayLayout::new691–703 ↗
fn new(
        command_continuation: PrefixedBlock,
        command_continuation_max_lines: usize,
        output_block: PrefixedBlock,
        output_max_lines: usize,
    ) -> Self

作用:创建一套执行命令显示布局配置,比如命令续行最多几行、输出最多几行。

数据流:输入命令续行块、命令续行上限、输出块、输出上限 → 它保存成一个布局对象 → 输出 ExecDisplayLayout。

调用关系:常量 EXEC_DISPLAY_LAYOUT 通过它定义默认布局;ExecCell::command_display_lines 每次渲染都会按这套规则排版。

tests::render_line_text719–724 ↗
fn render_line_text(line: &Line<'static>) -> String

作用:测试用的小工具,把带样式的 Line 还原成普通字符串,方便断言比较。

数据流:输入一个 Line → 它把里面每个 span 的文字内容拼起来,忽略颜色和粗体等样式 → 输出纯字符串。

调用关系:多个测试用它检查渲染结果里到底出现了哪些文字,而不是关心样式细节。

tests::user_shell_output_is_limited_by_screen_lines727–829 ↗
fn user_shell_output_is_limited_by_screen_lines()

作用:确认用户手动运行的命令即使输出超长 URL,也不会因为自动换行而刷出几百行。

数据流:测试构造两行极长 URL 输出和很窄的屏幕宽度 → 先证明不截断会超过限制,再渲染 ExecCell → 断言输出占用的屏幕行数被限制住,并且包含省略提示和 transcript 快捷键。

调用关系:它直接覆盖 output_lines、prefix_lines、adaptive_wrap_line 和 command_display_lines 组合起来的行为,是防止长输出回归失控的保护网。

调用图:调用 6 个内部函数(new, output_lines, prefix_lines, push_owned_lines, new, adaptive_wrap_line);外部调用 8 个(new, from, new, from, new, assert!, format!, vec!)。

tests::truncate_lines_middle_keeps_omitted_count_in_line_units832–858 ↗
fn truncate_lines_middle_keeps_omitted_count_in_line_units()

作用:确认省略提示里的数字按“逻辑行”计算,而不是按屏幕换行后的行数计算。

数据流:测试构造几行内容,其中一行很长会换成多屏幕行,并带已有省略数量 → 调用 truncate_lines_middle → 检查最终提示是省略了正确的逻辑行数。

调用关系:它专门验证 ExecCell::truncate_lines_middle 的计数规则,避免不同终端宽度下省略数字乱变。

调用图:外部调用 4 个(from, assert!, truncate_lines_middle, vec!)。

tests::output_lines_ellipsis_includes_transcript_hint861–888 ↗
fn output_lines_ellipsis_includes_transcript_hint()

作用:确认 output_lines 省略输出时,会告诉用户可以按 ctrl+t 看完整记录。

数据流:测试构造 7 行输出,并设置每端只保留 2 行 → 调用 output_lines → 检查结果里有“… +3 lines (ctrl + t to view transcript)”。

调用关系:它直接保护 output_lines 和 ExecCell::output_ellipsis_line 的用户提示行为。

调用图:调用 1 个内部函数(output_lines);外部调用 2 个(new, assert!)。

tests::command_truncation_ellipsis_does_not_include_transcript_hint891–910 ↗
fn command_truncation_ellipsis_does_not_include_transcript_hint()

作用:确认命令文本本身被截断时,只显示简单省略,不出现查看 transcript 的提示。

数据流:测试传入三行命令显示内容,只保留前两行 → 调用 limit_lines_from_start → 断言第三行提示是“… +1 lines”。

调用关系:它保护 ExecCell::limit_lines_from_start 和 ExecCell::ellipsis_line,确保命令截断和输出截断的提示文案不会混用。

调用图:外部调用 3 个(from, assert_eq!, limit_lines_from_start)。

tests::truncate_lines_middle_does_not_truncate_blank_prefixed_output_lines913–924 ↗
fn truncate_lines_middle_does_not_truncate_blank_prefixed_output_lines()

作用:确认一堆只有缩进空白的输出行不会被错误地当成超长内容截断。

数据流:测试构造 start、26 行空白缩进行、end,并给足最大行数 → 调用 truncate_lines_middle → 断言返回内容完全不变。

调用关系:它保护 ExecCell::truncate_lines_middle 对空白行的行数计算,避免空白输出被误判。

调用图:外部调用 5 个(from, assert_eq!, repeat_n, truncate_lines_middle, vec!)。

tests::command_display_does_not_split_long_url_token927–958 ↗
fn command_display_does_not_split_long_url_token()

作用:确认命令里出现很长 URL 时,不会被按连字符硬切成多段。

数据流:测试构造一条 echo 长 URL 的用户命令 → 渲染 command_display_lines → 检查完整 URL 只出现在一行里。

调用关系:它验证 ExecCell::command_display_lines 配置了不按连字符拆词的换行规则,防止 URL 被破坏。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, assert_eq!, vec!)。

tests::active_command_without_animations_is_stable961–987 ↗
fn active_command_without_animations_is_stable()

作用:确认关闭动画时,正在运行命令的显示是稳定不变的,便于测试和减少视觉跳动。

数据流:测试构造一个正在运行、但动画关闭的命令 → 连续渲染两次 → 断言两次结果相同,并显示静态圆点和 Running。

调用关系:它覆盖 activity_marker 和 ExecCell::command_display_lines 的配合,确保无动画模式不会偷偷变化。

调用图:调用 1 个内部函数(new);外部调用 4 个(now, new, assert_eq!, vec!)。

tests::exploring_display_does_not_split_long_url_like_search_query990–1027 ↗
fn exploring_display_does_not_split_long_url_like_search_query()

作用:确认探索视图里的长搜索词,尤其像 URL 的字符串,不会被拆开。

数据流:测试构造一个 Search 类型的解析命令,查询词很长 → 调用 display_lines → 检查完整查询词只出现在一行。

调用关系:它验证 ExecCell::display_lines 会走 exploring_display_lines,并且探索视图也使用不乱拆长 token 的换行策略。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, vec!)。

tests::output_display_does_not_split_long_url_like_token_without_scheme1030–1065 ↗
fn output_display_does_not_split_long_url_like_token_without_scheme()

作用:确认输出里像 URL 但没有 http 前缀的长字符串,也不会被拆得七零八落。

数据流:测试构造一个命令输出,内容是一段很长的路径式字符串 → 渲染 command_display_lines → 断言完整字符串只出现一次且保持完整。

调用关系:它保护输出区域的换行设置,覆盖 output_lines、adaptive_wrap_line 和 command_display_lines 的组合行为。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, new, assert_eq!, vec!)。

tests::desired_transcript_height_accounts_for_wrapped_url_like_rows1068–1094 ↗
fn desired_transcript_height_accounts_for_wrapped_url_like_rows()

作用:确认完整记录视图计算高度时,会考虑长 URL 自动换行后实际占用的屏幕行数。

数据流:测试构造带长 URL 输出的命令 → 分别取得 transcript_lines 的逻辑行数和 desired_transcript_height 的实际高度 → 断言实际高度更大。

调用关系:它虽然调用的是 ExecCell 的高度计算能力,但用本文件生成的 transcript_lines 来验证:完整记录的布局不能只按逻辑行数估算。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, assert!, vec!)。

tui/src/diff_render.rs源码 ↗
domain_logicrender frame / diff display / tests

这个文件像一个“改动展示排版师”。它先把每个文件的变化整理成行:新增文件算新增行,删除文件算删除行,修改文件会解析统一 diff(一种常见的补丁文本格式)。然后它给每行加上右对齐行号、左边的 + 或 - 标记,并根据终端背景是亮色还是暗色选择不同配色。终端颜色能力也会被考虑:真彩色、256 色、只有 16 色时用不同方案,避免颜色糊成一片。它还会按文件扩展名尝试语法高亮,比如 .rs 当 Rust 看;但 diff 太大时会主动跳过高亮,防止界面卡住。长行不会被硬挤出屏幕,而是按显示宽度换行,连中文、emoji、制表符都算进去。

函数细节99
RichDiffColorLevel::from_diff_color_level160–166 ↗
fn from_diff_color_level(level: DiffColorLevel) -> Option<Self>

作用:判断当前颜色能力够不够用“柔和背景色”。只有真彩色和 256 色算够,16 色太粗糙就不要背景色。

数据流:输入一个 DiffColorLevel → 它检查是不是 TrueColor 或 Ansi256 → 输出对应的 RichDiffColorLevel,或者在 Ansi16 时输出 None。

调用关系:配色相关函数会先问它“能不能上背景色”。fallback_diff_backgrounds、resolve_diff_backgrounds_for 和 style_gutter_for 都靠它避免在 16 色终端上画难看的大块背景。

调用图:被 3 处调用(fallback_diff_backgrounds, resolve_diff_backgrounds_for, style_gutter_for)。

resolve_diff_backgrounds199–204 ↗
fn resolve_diff_backgrounds(
    theme: DiffTheme,
    color_level: DiffColorLevel,
) -> ResolvedDiffBackgrounds

作用:给真实运行时准备新增行和删除行的背景色。它会看当前语法主题有没有专门的 diff 背景色。

数据流:输入终端明暗主题和颜色档位 → 读取语法主题里的 inserted/deleted 背景色 → 交给纯逻辑函数合成最终背景色 → 输出新增、删除各自的背景色。

调用关系:current_diff_render_style_context 在每次渲染前调用它。它把读取主题这件事包起来,再把真正计算交给 resolve_diff_backgrounds_for。

调用图:调用 2 个内部函数(resolve_diff_backgrounds_for, diff_scope_background_rgbs);被 1 处调用(current_diff_render_style_context)。

current_diff_render_style_context215–224 ↗
fn current_diff_render_style_context() -> DiffRenderStyleContext

作用:一次性收集本轮 diff 渲染要用的颜色环境。这样每一行不用反复探测终端和主题。

数据流:不接收业务输入 → 读取终端背景、终端颜色能力、语法主题 diff 背景 → 组装成 DiffRenderStyleContext 返回。

调用关系:render_change 在开始画一个改动块时调用它;主题预览和测试也会用它,保证同一帧里的配色一致。

调用图:调用 3 个内部函数(diff_color_level, diff_theme, resolve_diff_backgrounds);被 6 处调用(render_change, fallback_wrapping_uses_display_width_for_tabs_and_wide_chars, ui_snapshot_syntax_highlighted_insert_wraps, ui_snapshot_syntax_highlighted_insert_wraps_text, ui_snapshot_wrap_behavior_insert, render_preview)。

resolve_diff_backgrounds_for232–249 ↗
fn resolve_diff_backgrounds_for(
    theme: DiffTheme,
    color_level: DiffColorLevel,
    scope_backgrounds: DiffScopeBackgroundRgbs,
) -> ResolvedDiffBackgrounds

作用:用纯计算方式决定新增和删除行最终背景色,方便测试。规则是先有默认色,主题如果提供了更具体的颜色再覆盖。

数据流:输入明暗主题、颜色档位、主题里读到的 RGB 背景 → 先算默认背景 → 如果颜色够丰富且主题有指定颜色,就转换后覆盖 → 输出 ResolvedDiffBackgrounds。

调用关系:resolve_diff_backgrounds 调它做实际计算;多组测试直接调它验证主题覆盖、256 色压缩和 16 色禁用背景这些规则。

调用图:调用 3 个内部函数(from_diff_color_level, color_from_rgb_for_level, fallback_diff_backgrounds);被 5 处调用(resolve_diff_backgrounds, ansi16_disables_line_and_gutter_backgrounds, theme_scope_backgrounds_override_truecolor_fallback_when_available, theme_scope_backgrounds_quantize_to_ansi256, ui_snapshot_theme_scope_background_resolution)。

fallback_diff_backgrounds253–264 ↗
fn fallback_diff_backgrounds(
    theme: DiffTheme,
    color_level: DiffColorLevel,
) -> ResolvedDiffBackgrounds

作用:在语法主题没提供 diff 背景色时,给新增和删除行提供内置默认底色。16 色终端则不提供背景。

数据流:输入明暗主题和颜色档位 → 判断是否支持丰富背景色 → 支持就分别取新增、删除背景色,不支持就返回空背景。

调用关系:resolve_diff_backgrounds_for 把它当基础色板;样式测试也用它确认 16 色模式不会误上背景。

调用图:调用 3 个内部函数(from_diff_color_level, add_line_bg, del_line_bg);被 6 处调用(resolve_diff_backgrounds_for, ansi16_add_style_uses_foreground_only, ansi16_del_style_uses_foreground_only, ansi16_sign_styles_use_foreground_only, light_theme_wrapped_lines_keep_number_gutter_contrast, ui_snapshot_ansi16_insert_delete_no_background);外部调用 1 个(default)。

color_from_rgb_for_level268–273 ↗
fn color_from_rgb_for_level(rgb: (u8, u8, u8), color_level: RichDiffColorLevel) -> Color

作用:把一个 RGB 颜色变成当前终端能显示的颜色格式。真彩色直接用,256 色就找最接近的颜色。

数据流:输入 RGB 三元组和丰富颜色档位 → TrueColor 时转成 RGB Color,Ansi256 时量化成索引色 → 输出 ratatui 可用的 Color。

调用关系:resolve_diff_backgrounds_for 在主题给了自定义背景色时调用它,把主题颜色适配到终端能力。

调用图:调用 2 个内部函数(quantize_rgb_to_ansi256, rgb_color);被 1 处调用(resolve_diff_backgrounds_for)。

quantize_rgb_to_ansi256281–294 ↗
fn quantize_rgb_to_ansi256(target: (u8, u8, u8)) -> Color

作用:在 256 色调色板里找一个最像目标 RGB 的颜色。就像拿着颜料样本去有限色卡里挑最接近的一张。

数据流:输入目标 RGB → 遍历 xterm 256 色表中稳定的 16 到 255 号颜色 → 用感知距离比较相似度 → 输出最接近的索引色。

调用关系:color_from_rgb_for_level 在 Ansi256 模式下调用它,保证主题颜色在低一些的颜色能力下也尽量接近原样。

调用图:调用 1 个内部函数(indexed_color);被 1 处调用(color_from_rgb_for_level)。

DiffSummary::new302–304 ↗
fn new(changes: HashMap<PathBuf, FileChange>, cwd: AbsolutePathBuf) -> Self

作用:创建一个 diff 摘要对象,里面有所有文件变化和当前工作目录。后面渲染时要靠它知道路径该怎么显示。

数据流:输入文件变化表和当前目录 → 原样存进 DiffSummary → 返回这个摘要对象。

调用关系:它是构造入口,之后 DiffSummary 可以被转成 Box<dyn Renderable>,真正画到界面里。

FileChange::render308–312 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把单个文件的变化画到终端缓冲区里。它是 FileChange 作为可渲染组件时的实际绘制动作。

数据流:输入绘制区域和缓冲区 → 调 render_change 生成多行带样式文本 → 用 Paragraph 写进缓冲区。

调用关系:TUI 框架需要画 FileChange 时会调用它;它把细节交给 render_change。

调用图:调用 1 个内部函数(render_change);外部调用 2 个(new, vec!)。

FileChange::desired_height314–318 ↗
fn desired_height(&self, width: u16) -> u16

作用:提前估算这个文件变化画出来需要多少行高。界面排版要靠它决定给组件留多大地方。

数据流:输入可用宽度 → 调 render_change 生成渲染行但不真正画 → 返回行数。

调用关系:布局系统在渲染前会调用它;它和 FileChange::render 使用同一个 render_change,避免估算和实际显示不一致。

调用图:调用 1 个内部函数(render_change);外部调用 1 个(vec!)。

Box::from322–343 ↗
fn from(val: DiffSummary) -> Self

作用:把整个 DiffSummary 变成一个可放进界面的渲染对象。它会按文件分块,加标题、空行和缩进。

数据流:输入 DiffSummary → 收集并排序每个文件的行信息 → 生成路径标题、行数摘要和缩进后的 diff 块 → 输出一个纵向排列的 Renderable。

调用关系:这是摘要进入 TUI 组件系统的桥梁;它调用 collect_rows、display_path_for、render_line_count_summary,并把每个 FileChange 包进 InsetRenderable。

调用图:调用 6 个内部函数(collect_rows, display_path_for, render_line_count_summary, tlbr, with, new);外部调用 3 个(new, from, vec!)。

create_diff_summary346–353 ↗
fn create_diff_summary(
    changes: &HashMap<PathBuf, FileChange>,
    cwd: &Path,
    wrap_cols: usize,
) -> Vec<RtLine<'static>>

作用:生成一组可显示的 diff 摘要行。适合命令结果、测试快照或其他不需要完整组件对象的地方使用。

数据流:输入文件变化表、当前目录、换行宽度 → collect_rows 整理文件 → render_changes_block 画成文本行 → 返回行列表。

调用关系:很多测试直接用它验证最终显示效果;生产路径也可以用它拿到已经排好版的 diff 文本。

调用图:调用 2 个内部函数(collect_rows, render_changes_block);被 12 处调用(add_diff_uses_path_extension_for_highlighting, delete_diff_uses_path_extension_for_highlighting, diff_summary_for_tests, large_update_diff_skips_highlighting, rename_diff_uses_destination_extension_for_highlighting, snapshot_diff_gallery, ui_snapshot_apply_update_block_line_numbers_three_digits_text, ui_snapshot_apply_update_block_relativizes_path, ui_snapshot_apply_update_block_wraps_long_lines, ui_snapshot_apply_update_block_wraps_long_lines_text (+2 more))。

collect_rows366–391 ↗
fn collect_rows(changes: &HashMap<PathBuf, FileChange>) -> Vec<Row>

作用:把原始文件变化表整理成按文件显示的行资料。它会算每个文件新增多少行、删除多少行,以及是否重命名。

数据流:输入路径到 FileChange 的表 → 对新增/删除直接数行,对修改解析 diff 统计加删 → 记录重命名目标 → 按路径排序后输出 Row 列表。

调用关系:create_diff_summary 和 Box::from 都先调用它,把散乱的 HashMap 变成稳定顺序的展示数据。

调用图:调用 1 个内部函数(calculate_add_remove_from_diff);被 2 处调用(from, create_diff_summary);外部调用 1 个(new)。

render_line_count_summary393–401 ↗
fn render_line_count_summary(added: usize, removed: usize) -> Vec<RtSpan<'static>>

作用:生成类似“(+3 -1)”这样的行数摘要,并给新增数字染绿、删除数字染红。

数据流:输入新增行数和删除行数 → 拼出括号、加号数字、减号数字这些 span → 输出一组带样式的小片段。

调用关系:文件标题和总标题都调用它,让用户一眼看到改动规模。

调用图:被 2 处调用(from, render_changes_block);外部调用 2 个(new, format!)。

render_changes_block403–465 ↗
fn render_changes_block(rows: Vec<Row>, wrap_cols: usize, cwd: &Path) -> Vec<RtLine<'static>>

作用:把多个文件的变化排成完整的 diff 展示块。它负责总标题、每个文件标题、缩进和每行内容的串联。

数据流:输入 Row 列表、换行宽度、当前目录 → 统计总加删行 → 写总标题 → 每个文件检测语言、渲染改动、加缩进 → 输出所有显示行。

调用关系:create_diff_summary 把整理好的 rows 交给它;它再调用 detect_lang_for_path、render_change 和 prefix_lines 完成最终排版。

调用图:调用 4 个内部函数(detect_lang_for_path, render_change, render_line_count_summary, prefix_lines);被 1 处调用(create_diff_summary);外部调用 4 个(from, new, format!, vec!)。

detect_lang_for_path470–473 ↗
fn detect_lang_for_path(path: &Path) -> Option<String>

作用:从文件路径里取扩展名,用来猜代码语言。比如 foo.rs 会返回 rs,让高亮模块再判断是不是 Rust。

数据流:输入路径 → 取最后的扩展名并转成字符串 → 有扩展名就返回 Some,没有就返回 None。

调用关系:render_changes_block 在渲染每个文件前调用它;重命名文件会用新路径扩展名,因为 diff 内容代表新文件。

调用图:被 1 处调用(render_changes_block);外部调用 1 个(extension)。

render_change475–737 ↗
fn render_change(
    change: &FileChange,
    out: &mut Vec<RtLine<'static>>,
    width: usize,
    lang: Option<&str>,
)

作用:把单个文件的新增、删除或修改内容变成一行行带行号、符号、颜色的显示文本。这是本文件最核心的渲染流程。

数据流:输入 FileChange、输出行数组、可用宽度、可选语言 → 获取本轮配色上下文 → 新增/删除直接逐行渲染,修改则解析统一 diff、维护新旧行号、按 hunk 高亮和渲染 → 把结果追加到 out。

调用关系:FileChange::render、FileChange::desired_height、render_changes_block 都调用它;它再把单行渲染交给 push_wrapped_diff_line_inner_with_theme_and_color_level。

调用图:调用 5 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_inner_with_theme_and_color_level, style_gutter_for, exceeds_highlight_limits);被 3 处调用(desired_height, render, render_changes_block);外部调用 5 个(from, styled, from_str, format!, vec!)。

display_path_for742–763 ↗
fn display_path_for(path: &Path, cwd: &Path) -> String

作用:把文件路径变成更适合人看的形式。能相对当前目录显示就不用绝对路径,避免界面里出现很长的一串。

数据流:输入文件路径和当前目录 → 如果本来就是相对路径直接显示;如果在当前目录下就裁掉前缀;否则判断是否同一 git 仓库或能否写成 ~ 路径 → 输出最终字符串。

调用关系:文件标题里会调用它;测试 display_path_prefers_cwd_without_git_repo 验证没有 .git 的工作区也能显示短路径。

调用图:调用 1 个内部函数(relativize_to_home);被 2 处调用(from, display_path_prefers_cwd_without_git_repo);外部调用 5 个(display, is_relative, strip_prefix, get_git_repo_root, diff_paths)。

calculate_add_remove_from_diff765–780 ↗
fn calculate_add_remove_from_diff(diff: &str) -> (usize, usize)

作用:从统一 diff 文本里数出新增行和删除行。解析失败时安全返回 0,避免界面崩掉。

数据流:输入 diff 字符串 → 尝试解析成 Patch → 遍历所有 hunk 的行,Insert 加新增计数,Delete 加删除计数 → 输出两个数字。

调用关系:collect_rows 在遇到 Update 文件变化时调用它,用来生成标题里的“+几 -几”。

调用图:被 1 处调用(collect_rows);外部调用 1 个(from_str)。

push_wrapped_diff_line_with_style_context788–807 ↗
fn push_wrapped_diff_line_with_style_context(
    line_number: usize,
    kind: DiffLineType,
    text: &str,
    width: usize,
    line_number_width: usize,
    style_context: DiffRenderStyleContext,

作用:渲染一条没有语法高亮的 diff 行,并按宽度自动换行。调用者只需要传已经算好的配色上下文。

数据流:输入行号、行类型、文本、宽度、行号宽度、配色上下文 → 设置 syntax_spans 为 None → 调内部核心函数 → 返回一行或多行 RtLine。

调用关系:主题预览和部分测试用它展示普通 diff 行;真正工作由 push_wrapped_diff_line_inner_with_theme_and_color_level 完成。

调用图:调用 1 个内部函数(push_wrapped_diff_line_inner_with_theme_and_color_level);被 3 处调用(fallback_wrapping_uses_display_width_for_tabs_and_wide_chars, ui_snapshot_wrap_behavior_insert, render_preview)。

push_wrapped_diff_line_with_syntax_and_style_context816–836 ↗
fn push_wrapped_diff_line_with_syntax_and_style_context(
    line_number: usize,
    kind: DiffLineType,
    text: &str,
    width: usize,
    line_number_width: usize,
    syntax_spans: &[RtSpan<'sta

作用:渲染一条带语法高亮的 diff 行,并且保留换行后的颜色。删除行会额外变暗,强调这是被删的内容。

数据流:输入行号、行类型、原文本、宽度、行号宽度、语法高亮片段、配色上下文 → 把语法片段交给内部核心函数 → 输出换行后的带样式行。

调用关系:语法高亮预览和测试直接用它;render_change 内部使用同一套更底层逻辑处理真实 diff。

调用图:调用 1 个内部函数(push_wrapped_diff_line_inner_with_theme_and_color_level);被 3 处调用(ui_snapshot_syntax_highlighted_insert_wraps, ui_snapshot_syntax_highlighted_insert_wraps_text, render_preview)。

push_wrapped_diff_line_inner_with_theme_and_color_level839–939 ↗
fn push_wrapped_diff_line_inner_with_theme_and_color_level(
    line_number: usize,
    kind: DiffLineType,
    text: &str,
    width: usize,
    line_number_width: usize,
    syntax_spans: Option<&[R

作用:真正把一条 diff 行排版成终端行。它负责行号栏、+/- 符号、内容颜色、背景色和长行换行。

数据流:输入行号、插入/删除/上下文类型、文本、宽度、可选语法片段和配色信息 → 算出 gutter、sign、content 的样式 → 用 wrap_styled_spans 切成多行 → 输出 RtLine 列表。

调用关系:render_change 和两个公开包装函数都把单行渲染交给它;它再调用各种 style_* 函数和 wrap_styled_spans。

调用图:调用 8 个内部函数(style_add, style_context, style_del, style_gutter_for, style_line_bg_for, style_sign_add, style_sign_del, wrap_styled_spans);被 5 处调用(push_wrapped_diff_line_with_style_context, push_wrapped_diff_line_with_syntax_and_style_context, render_change, light_theme_wrapped_lines_keep_number_gutter_contrast, ui_snapshot_ansi16_insert_delete_no_background);外部调用 5 个(from, styled, new, format!, vec!)。

wrap_styled_spans952–1021 ↗
fn wrap_styled_spans(spans: &[RtSpan<'static>], max_cols: usize) -> Vec<Vec<RtSpan<'static>>>

作用:把带样式的文字片段按显示宽度拆成多行,同时不丢颜色。它会正确处理中文、emoji、制表符这类不等宽字符。

数据流:输入一组带样式 span 和最大列宽 → 逐字符计算显示宽度 → 超宽时切到下一行,并把原来的样式复制过去 → 输出每行的 span 列表。

调用关系:单行 diff 渲染核心调用它做换行;多组测试专门验证长文本、样式保留、tab 和宽字符。

调用图:被 7 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, wrap_styled_spans_flushes_at_span_boundary, wrap_styled_spans_preserves_styles, wrap_styled_spans_single_line, wrap_styled_spans_splits_long_content, wrap_styled_spans_tabs_have_visible_width, wrap_styled_spans_wraps_before_first_overflowing_char);外部调用 3 个(styled, new, take)。

line_number_width1023–1029 ↗
fn line_number_width(max_line_number: usize) -> usize

作用:算行号栏需要几位宽。比如最大行号 100 就要 3 位,保证所有行号右对齐。

数据流:输入最大行号 → 0 时返回 1,否则把数字转成字符串取长度 → 输出宽度。

调用关系:render_change 在渲染文件前用它统一行号栏宽度;测试和预览也用它构造样例。

调用图:被 8 处调用(render_change, fallback_wrapping_uses_display_width_for_tabs_and_wide_chars, light_theme_wrapped_lines_keep_number_gutter_contrast, ui_snapshot_ansi16_insert_delete_no_background, ui_snapshot_syntax_highlighted_insert_wraps, ui_snapshot_syntax_highlighted_insert_wraps_text, ui_snapshot_wrap_behavior_insert, render_preview)。

diff_theme_for_bg1032–1039 ↗
fn diff_theme_for_bg(bg: Option<(u8, u8, u8)>) -> DiffTheme

作用:根据一个背景色判断 diff 应该用亮色主题还是暗色主题。背景未知时默认暗色,比较稳妥。

数据流:输入可选 RGB 背景 → 如果有颜色且判断为亮色就返回 Light → 否则返回 Dark。

调用关系:diff_theme 读取终端背景后把结果交给它判断;这样判断逻辑可以单独测试。

调用图:调用 1 个内部函数(is_light);被 1 处调用(diff_theme)。

diff_theme1042–1044 ↗
fn diff_theme() -> DiffTheme

作用:探测当前终端背景,决定 diff 配色走亮色还是暗色路线。

数据流:不接收输入 → 读取默认背景色 default_bg → 调 diff_theme_for_bg → 输出 DiffTheme。

调用关系:current_diff_render_style_context 在每轮渲染开始时调用它,给后续所有样式函数定基调。

调用图:调用 2 个内部函数(diff_theme_for_bg, default_bg);被 1 处调用(current_diff_render_style_context)。

diff_color_level1054–1061 ↗
fn diff_color_level() -> DiffColorLevel

作用:判断当前终端适合用真彩色、256 色还是 16 色来画 diff。它还特别照顾 Windows Terminal 的真实能力。

数据流:不接收输入 → 读取 stdout 颜色能力、终端名称、WT_SESSION、FORCE_COLOR → 调 diff_color_level_for_terminal → 输出 DiffColorLevel。

调用关系:current_diff_render_style_context 调它拿颜色档位;它自己只负责读环境,具体策略交给 diff_color_level_for_terminal。

调用图:调用 3 个内部函数(diff_color_level_for_terminal, has_force_color_override, stdout_color_level);被 1 处调用(current_diff_render_style_context);外部调用 2 个(terminal_info, var_os)。

has_force_color_override1064–1066 ↗
fn has_force_color_override() -> bool

作用:检查用户是否显式设置了 FORCE_COLOR。这个变量表示用户想自己控制颜色能力判断。

数据流:不接收输入 → 查询环境变量 FORCE_COLOR 是否存在 → 返回布尔值。

调用关系:diff_color_level 用它决定是否尊重用户强制设置,避免自动把 Windows Terminal 提升到真彩色。

调用图:被 1 处调用(diff_color_level);外部调用 1 个(var_os)。

diff_color_level_for_terminal1090–1116 ↗
fn diff_color_level_for_terminal(
    stdout_level: StdoutColorLevel,
    terminal_name: TerminalName,
    has_wt_session: bool,
    has_force_color_override: bool,
) -> DiffColorLevel

作用:把原始终端颜色报告转成 diff 渲染真正使用的颜色档位。它包含 Windows Terminal 的特殊提升规则。

数据流:输入 stdout 颜色等级、终端名称、是否有 WT_SESSION、是否有 FORCE_COLOR → 先处理 Windows Terminal 提升例外 → 再按原始等级映射 → 输出 DiffColorLevel。

调用关系:diff_color_level 负责读环境后调用它;多组测试直接验证各种终端和环境变量组合。

调用图:被 1 处调用(diff_color_level)。

style_line_bg_for1141–1151 ↗
fn style_line_bg_for(kind: DiffLineType, diff_backgrounds: ResolvedDiffBackgrounds) -> Style

作用:决定整行 diff 的背景色。新增和删除行可能有底色,上下文行保持默认背景。

数据流:输入行类型和已解析背景色 → Insert 取 add 背景,Delete 取 del 背景,Context 返回默认样式 → 输出 Style。

调用关系:单行渲染核心把它设到 RtLine 上,让背景延伸到整行宽度。

调用图:被 1 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level);外部调用 1 个(default)。

style_context1153–1155 ↗
fn style_context() -> Style

作用:返回普通上下文行的默认样式。也就是不额外染色。

数据流:不接收输入 → 创建默认 Style → 返回。

调用关系:push_wrapped_diff_line_inner_with_theme_and_color_level 在处理未改动上下文行时使用它。

调用图:被 1 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level);外部调用 1 个(default)。

add_line_bg1157–1164 ↗
fn add_line_bg(theme: DiffTheme, color_level: RichDiffColorLevel) -> Color

作用:给新增行挑一款默认背景色。暗色终端用深绿,亮色终端用浅绿。

数据流:输入明暗主题和丰富颜色档位 → 根据真彩色或 256 色选择 RGB 或索引色 → 输出 Color。

调用关系:fallback_diff_backgrounds 在没有主题自定义背景时调用它。

调用图:调用 2 个内部函数(indexed_color, rgb_color);被 1 处调用(fallback_diff_backgrounds)。

del_line_bg1166–1173 ↗
fn del_line_bg(theme: DiffTheme, color_level: RichDiffColorLevel) -> Color

作用:给删除行挑一款默认背景色。暗色终端用深红,亮色终端用浅红。

数据流:输入明暗主题和丰富颜色档位 → 根据真彩色或 256 色选择 RGB 或索引色 → 输出 Color。

调用关系:fallback_diff_backgrounds 在没有主题自定义背景时调用它。

调用图:调用 2 个内部函数(indexed_color, rgb_color);被 1 处调用(fallback_diff_backgrounds)。

light_gutter_fg1175–1181 ↗
fn light_gutter_fg(color_level: DiffColorLevel) -> Color

作用:给亮色主题下的行号栏选择文字颜色。它要足够深,才能压住浅色背景。

数据流:输入颜色档位 → 真彩色用指定深灰 RGB,256 色用相近索引色,16 色用黑色 → 输出 Color。

调用关系:style_gutter_for 在亮色主题下调用它,确保行号可读。

调用图:调用 2 个内部函数(indexed_color, rgb_color);被 1 处调用(style_gutter_for)。

light_add_num_bg1183–1188 ↗
fn light_add_num_bg(color_level: RichDiffColorLevel) -> Color

作用:给亮色主题下新增行的行号栏选择更浓一点的绿色背景。这样行号不会淹没在浅绿整行背景里。

数据流:输入丰富颜色档位 → 真彩色返回 RGB 浅绿,256 色返回索引色 → 输出 Color。

调用关系:style_gutter_for 在亮色新增行且支持背景色时调用它。

调用图:调用 2 个内部函数(indexed_color, rgb_color);被 1 处调用(style_gutter_for)。

light_del_num_bg1190–1195 ↗
fn light_del_num_bg(color_level: RichDiffColorLevel) -> Color

作用:给亮色主题下删除行的行号栏选择更浓一点的红色背景。这样删除行号更容易看清。

数据流:输入丰富颜色档位 → 真彩色返回 RGB 浅红,256 色返回索引色 → 输出 Color。

调用关系:style_gutter_for 在亮色删除行且支持背景色时调用它。

调用图:调用 2 个内部函数(indexed_color, rgb_color);被 1 处调用(style_gutter_for)。

style_gutter_for1200–1220 ↗
fn style_gutter_for(kind: DiffLineType, theme: DiffTheme, color_level: DiffColorLevel) -> Style

作用:决定行号栏的样式。亮色主题会给新增/删除行号更明显的背景,暗色主题通常只把行号变淡。

数据流:输入行类型、明暗主题、颜色档位 → 判断是否支持丰富背景 → 亮色新增/删除使用专门前景和背景,否则走默认或变淡 → 输出 Style。

调用关系:render_change 画 hunk 分隔行和单行渲染核心都用它;它会调用 light_* 和 style_gutter_dim。

调用图:调用 5 个内部函数(from_diff_color_level, light_add_num_bg, light_del_num_bg, light_gutter_fg, style_gutter_dim);被 2 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, render_change);外部调用 1 个(default)。

style_sign_add1225–1234 ↗
fn style_sign_add(
    theme: DiffTheme,
    color_level: DiffColorLevel,
    diff_backgrounds: ResolvedDiffBackgrounds,
) -> Style

作用:决定新增行左边 + 号的样式。亮色主题只用绿色字,暗色主题则跟新增内容样式保持一致。

数据流:输入主题、颜色档位、背景色 → 亮色返回绿色前景;暗色调用 style_add → 输出 Style。

调用关系:单行渲染核心在画 + 号时调用它;相关测试确认 16 色下不会带背景。

调用图:调用 1 个内部函数(style_add);被 2 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, ansi16_sign_styles_use_foreground_only);外部调用 1 个(default)。

style_sign_del1237–1246 ↗
fn style_sign_del(
    theme: DiffTheme,
    color_level: DiffColorLevel,
    diff_backgrounds: ResolvedDiffBackgrounds,
) -> Style

作用:决定删除行左边 - 号的样式。亮色主题只用红色字,暗色主题则跟删除内容样式保持一致。

数据流:输入主题、颜色档位、背景色 → 亮色返回红色前景;暗色调用 style_del → 输出 Style。

调用关系:单行渲染核心在画 - 号时调用它;相关测试确认 16 色下不会带背景。

调用图:调用 1 个内部函数(style_del);被 2 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, ansi16_sign_styles_use_foreground_only);外部调用 1 个(default)。

style_add1259–1277 ↗
fn style_add(
    theme: DiffTheme,
    color_level: DiffColorLevel,
    diff_backgrounds: ResolvedDiffBackgrounds,
) -> Style

作用:决定新增内容本身的样式。颜色能力差时只用绿色字,能力好时可以加绿色系背景。

数据流:输入主题、颜色档位、已解析背景 → 16 色返回绿色前景;亮色富颜色用背景;暗色富颜色用绿色前景加背景;没背景时退化为可读样式 → 输出 Style。

调用关系:单行渲染核心和 style_sign_add 都使用它;测试验证 16 色模式只用前景色。

调用图:被 3 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, style_sign_add, ansi16_add_style_uses_foreground_only);外部调用 1 个(default)。

style_del1283–1301 ↗
fn style_del(
    theme: DiffTheme,
    color_level: DiffColorLevel,
    diff_backgrounds: ResolvedDiffBackgrounds,
) -> Style

作用:决定删除内容本身的样式。它和新增样式类似,但用红色系。

数据流:输入主题、颜色档位、已解析背景 → 16 色返回红色前景;富颜色按亮暗主题决定是否加红色前景和背景 → 输出 Style。

调用关系:单行渲染核心和 style_sign_del 都使用它;测试验证 16 色模式不会画背景。

调用图:被 3 处调用(push_wrapped_diff_line_inner_with_theme_and_color_level, style_sign_del, ansi16_del_style_uses_foreground_only);外部调用 1 个(default)。

style_gutter_dim1303–1305 ↗
fn style_gutter_dim() -> Style

作用:给行号栏一个变淡样式。暗色主题下这样就足够区分行号和内容。

数据流:不接收输入 → 创建默认样式并加 DIM 修饰,也就是淡化 → 返回 Style。

调用关系:style_gutter_for 在不需要特殊亮色 gutter 时调用它。

调用图:被 1 处调用(style_gutter_for);外部调用 1 个(default)。

tests::ansi16_add_style_uses_foreground_only1321–1329 ↗
fn ansi16_add_style_uses_foreground_only()

作用:测试 16 色终端下新增内容只用绿色文字,不使用背景色。这样避免低色彩终端显示刺眼色块。

数据流:构造 16 色新增样式 → 检查前景是绿色、背景为空 → 测试通过或失败。

调用关系:它直接验证 fallback_diff_backgrounds 和 style_add 的配合。

调用图:调用 2 个内部函数(fallback_diff_backgrounds, style_add);外部调用 1 个(assert_eq!)。

tests::ansi16_del_style_uses_foreground_only1332–1340 ↗
fn ansi16_del_style_uses_foreground_only()

作用:测试 16 色终端下删除内容只用红色文字,不使用背景色。

数据流:构造 16 色删除样式 → 检查前景是红色、背景为空 → 输出测试结果。

调用关系:它覆盖 style_del 在低色彩模式下的退化规则。

调用图:调用 2 个内部函数(fallback_diff_backgrounds, style_del);外部调用 1 个(assert_eq!)。

tests::ansi16_sign_styles_use_foreground_only1343–1359 ↗
fn ansi16_sign_styles_use_foreground_only()

作用:测试 16 色终端里 + 和 - 符号也不能带背景。符号只靠绿色或红色区分。

数据流:分别生成新增和删除符号样式 → 检查颜色和背景 → 测试通过或报错。

调用关系:它验证 style_sign_add、style_sign_del 和 fallback_diff_backgrounds 的组合。

调用图:调用 3 个内部函数(fallback_diff_backgrounds, style_sign_add, style_sign_del);外部调用 1 个(assert_eq!)。

tests::diff_summary_for_tests1360–1362 ↗
fn diff_summary_for_tests(changes: &HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>>

作用:给测试用的快捷函数,用固定当前目录和宽度生成 diff 摘要。

数据流:输入变化表 → 调 create_diff_summary,cwd 固定为根目录、宽度为 80 → 返回显示行。

调用关系:多个快照测试用它减少重复代码。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 1 个(from)。

tests::snapshot_lines1364–1374 ↗
fn snapshot_lines(name: &str, lines: Vec<RtLine<'static>>, width: u16, height: u16)

作用:把一组终端行渲染到测试终端里,并保存快照。快照像截图一样检查 UI 有没有变。

数据流:输入快照名、行、宽高 → 创建测试终端 → 渲染 Paragraph → 和保存的快照比较。

调用关系:大量 UI snapshot 测试调用它确认 diff 视觉排版。

调用图:外部调用 3 个(new, assert_snapshot!, new)。

tests::display_width1376–1380 ↗
fn display_width(text: &str) -> usize

作用:测试里用来计算字符串实际占多少终端列。它特别把 tab 当 4 列算。

数据流:输入字符串 → 逐字符取 Unicode 显示宽度,tab 用 TAB_WIDTH → 输出总列数。

调用关系:line_display_width 调用它检查换行后的行没有超过宽度。

tests::line_display_width1382–1387 ↗
fn line_display_width(line: &RtLine<'static>) -> usize

作用:计算一整行 RtLine 在终端里占的列数。用于验证换行函数真的控制住宽度。

数据流:输入 RtLine → 遍历每个 span 的文本并求显示宽度 → 输出总宽度。

调用关系:fallback_wrapping_uses_display_width_for_tabs_and_wide_chars 用它检查复杂字符换行。

tests::snapshot_lines_text1389–1404 ↗
fn snapshot_lines_text(name: &str, lines: &[RtLine<'static>])

作用:把带样式行转成纯文本快照。适合检查缩进、行号和换行位置,不关心颜色。

数据流:输入快照名和行列表 → 拼接每行 span 文本,去掉行尾空格 → 做文本快照比较。

调用关系:若干换行和行号测试用它,比终端截图更容易看结构。

调用图:外部调用 2 个(iter, assert_snapshot!)。

tests::display_path_prefers_cwd_without_git_repo1472–1489 ↗
fn display_path_prefers_cwd_without_git_repo()

作用:测试没有 git 仓库时,当前目录下的绝对路径仍会显示成相对路径。

数据流:构造 cwd 和 cwd 下的文件路径 → 调 display_path_for → 断言结果是短路径。

调用关系:它保护 display_path_for 的一个重要用户体验:不要无故显示长绝对路径。

调用图:调用 1 个内部函数(display_path_for);外部调用 3 个(from, assert_eq!, cfg!)。

tests::ui_snapshot_wrap_behavior_insert1492–1513 ↗
fn ui_snapshot_wrap_behavior_insert()

作用:测试新增长行会被正确换行。第一行有行号和 +,后续行对齐缩进。

数据流:输入一条长文本样例 → 调单行渲染函数 → 渲染成快照。

调用关系:它覆盖 current_diff_render_style_context、line_number_width 和 push_wrapped_diff_line_with_style_context 的组合。

调用图:调用 3 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_with_style_context);外部调用 1 个(snapshot_lines)。

tests::ui_snapshot_apply_update_block1516–1538 ↗
fn ui_snapshot_apply_update_block()

作用:测试一个普通修改文件的 diff 块显示效果。

数据流:构造原文和修改后文本 → 生成 patch → create_diff_summary 生成行 → 快照比较。

调用关系:它验证 render_change 处理 Update 的基本路径。

调用图:外部调用 5 个(new, from, create_patch, diff_summary_for_tests, snapshot_lines)。

tests::ui_snapshot_apply_update_with_rename_block1541–1563 ↗
fn ui_snapshot_apply_update_with_rename_block()

作用:测试文件重命名加内容修改时,标题和 diff 是否正确显示。

数据流:构造旧文件名、新文件名和 patch → 生成摘要行 → 快照比较。

调用关系:它覆盖 collect_rows 的 move_path 记录和 render_changes_block 的重命名路径显示。

调用图:外部调用 5 个(new, from, create_patch, diff_summary_for_tests, snapshot_lines)。

tests::ui_snapshot_apply_multiple_files_block1566–1596 ↗
fn ui_snapshot_apply_multiple_files_block()

作用:测试多个文件一起变化时的总标题和每个文件分块。

数据流:构造一个修改文件和一个新增文件 → 生成摘要 → 快照比较。

调用关系:它验证 collect_rows 排序、总加删统计和多文件标题逻辑。

调用图:外部调用 5 个(new, from, create_patch, diff_summary_for_tests, snapshot_lines)。

tests::ui_snapshot_apply_add_block1599–1616 ↗
fn ui_snapshot_apply_add_block()

作用:测试新增文件的 diff 显示。

数据流:构造一个 Add 文件变化 → 生成摘要行 → 快照比较。

调用关系:它覆盖 render_change 的 Add 分支。

调用图:外部调用 4 个(new, from, diff_summary_for_tests, snapshot_lines)。

tests::ui_snapshot_apply_delete_block1619–1635 ↗
fn ui_snapshot_apply_delete_block()

作用:测试删除文件的 diff 显示。

数据流:构造一个 Delete 文件变化 → 生成摘要行 → 快照比较。

调用关系:它覆盖 render_change 的 Delete 分支。

调用图:外部调用 4 个(new, from, diff_summary_for_tests, snapshot_lines)。

tests::ui_snapshot_apply_update_block_wraps_long_lines1638–1662 ↗
fn ui_snapshot_apply_update_block_wraps_long_lines()

作用:测试修改 diff 里很长的行会按指定宽度换行。

数据流:构造包含超长修改行的 patch → create_diff_summary 用较窄宽度渲染 → 终端快照比较。

调用关系:它验证 render_change 到 push_wrapped_diff_line_inner_with_theme_and_color_level 的换行路径。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 4 个(new, from, create_patch, snapshot_lines)。

tests::ui_snapshot_apply_update_block_wraps_long_lines_text1665–1683 ↗
fn ui_snapshot_apply_update_block_wraps_long_lines_text()

作用:用纯文本快照检查长行换行后的符号和缩进位置。

数据流:构造会换行的修改 patch → 按 28 列渲染 → 转纯文本快照。

调用关系:它重点保护“只有第一段有 +,后续段对齐”的显示规则。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 4 个(new, from, create_patch, snapshot_lines_text)。

tests::ui_snapshot_apply_update_block_line_numbers_three_digits_text1686–1710 ↗
fn ui_snapshot_apply_update_block_line_numbers_three_digits_text()

作用:测试三位数行号时,行号栏仍然对齐。

数据流:构造 110 行文本并修改第 100 行 → 生成 diff 摘要 → 做纯文本快照。

调用关系:它验证 line_number_width 和 render_change 的行号宽度计算。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 4 个(new, from, create_patch, snapshot_lines_text)。

tests::ui_snapshot_apply_update_block_relativizes_path1713–1739 ↗
fn ui_snapshot_apply_update_block_relativizes_path()

作用:测试绝对路径会按当前目录缩短显示,重命名目标也一样。

数据流:构造 cwd 下的绝对旧路径和新路径 → 渲染 diff 摘要 → 快照比较。

调用关系:它覆盖 display_path_for 在 render_changes_block 中的实际使用。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 4 个(new, create_patch, current_dir, snapshot_lines)。

tests::ui_snapshot_syntax_highlighted_insert_wraps1742–1773 ↗
fn ui_snapshot_syntax_highlighted_insert_wraps()

作用:测试带语法高亮的长 Rust 行也能换行,而不是被截断。

数据流:先生成 Rust 语法高亮 spans → 调带语法的单行渲染 → 断言输出多行并做快照。

调用关系:它验证 push_wrapped_diff_line_with_syntax_and_style_context 和 wrap_styled_spans 能保留样式。

调用图:调用 4 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_with_syntax_and_style_context, highlight_code_to_styled_spans);外部调用 2 个(assert!, snapshot_lines)。

tests::ui_snapshot_syntax_highlighted_insert_wraps_text1776–1794 ↗
fn ui_snapshot_syntax_highlighted_insert_wraps_text()

作用:用纯文本快照检查带语法高亮长行的换行位置。

数据流:生成 Rust 高亮 spans → 渲染带语法 diff 行 → 转成纯文本快照。

调用关系:它和前一个测试互补,一个看终端效果,一个看文本结构。

调用图:调用 4 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_with_syntax_and_style_context, highlight_code_to_styled_spans);外部调用 1 个(snapshot_lines_text)。

tests::ui_snapshot_ansi16_insert_delete_no_background1816–1846 ↗
fn ui_snapshot_ansi16_insert_delete_no_background()

作用:测试 16 色模式下新增和删除行没有背景色的快照效果。

数据流:手动用 Ansi16 渲染一条新增和一条删除 → 快照比较。

调用关系:它直接调用底层单行渲染核心,覆盖低色彩终端的视觉退化。

调用图:调用 3 个内部函数(fallback_diff_backgrounds, line_number_width, push_wrapped_diff_line_inner_with_theme_and_color_level);外部调用 1 个(snapshot_lines)。

tests::truecolor_dark_theme_uses_configured_backgrounds1849–1880 ↗
fn truecolor_dark_theme_uses_configured_backgrounds()

作用:测试暗色真彩色主题使用预设的深绿和深红背景。

数据流:分别取新增、删除行背景和 gutter 样式 → 与预期样式比较。

调用关系:它保护 add_line_bg、del_line_bg、style_line_bg_for、style_gutter_for 的暗色真彩色规则。

调用图:外部调用 1 个(assert_eq!)。

tests::ansi256_dark_theme_uses_distinct_add_and_delete_backgrounds1883–1909 ↗
fn ansi256_dark_theme_uses_distinct_add_and_delete_backgrounds()

作用:测试暗色 256 色模式下新增和删除背景不同。否则用户很难区分加删。

数据流:生成新增和删除背景样式 → 检查各自等于预设索引色,并且彼此不同。

调用关系:它保护 256 色 fallback 色板。

调用图:外部调用 2 个(assert_eq!, assert_ne!)。

tests::theme_scope_backgrounds_override_truecolor_fallback_when_available1912–1929 ↗
fn theme_scope_backgrounds_override_truecolor_fallback_when_available()

作用:测试语法主题提供 diff 背景色时,会覆盖内置默认色。

数据流:输入自定义 inserted/deleted RGB → resolve_diff_backgrounds_for 解析 → 检查行背景就是这些 RGB。

调用关系:它验证主题覆盖逻辑在真彩色模式下生效。

调用图:调用 1 个内部函数(resolve_diff_backgrounds_for);外部调用 1 个(assert_eq!)。

tests::theme_scope_backgrounds_quantize_to_ansi2561932–1949 ↗
fn theme_scope_backgrounds_quantize_to_ansi256()

作用:测试主题 RGB 背景在 256 色模式下会被压到最接近的索引色。

数据流:输入 inserted RGB,deleted 留空 → 解析背景 → 检查新增用量化色、删除仍用默认色。

调用关系:它覆盖 resolve_diff_backgrounds_for 到 quantize_rgb_to_ansi256 的路径。

调用图:调用 1 个内部函数(resolve_diff_backgrounds_for);外部调用 1 个(assert_eq!)。

tests::ui_snapshot_theme_scope_background_resolution1952–1967 ↗
fn ui_snapshot_theme_scope_background_resolution()

作用:用快照记录主题背景解析后的结果。这样以后改配色策略会被明显发现。

数据流:构造主题只提供 inserted 背景 → 解析并格式化新增/删除背景 → 快照比较。

调用关系:它直接测试 resolve_diff_backgrounds_for 的覆盖和保底行为。

调用图:调用 1 个内部函数(resolve_diff_backgrounds_for);外部调用 2 个(assert_snapshot!, format!)。

tests::ansi16_disables_line_and_gutter_backgrounds1970–2017 ↗
fn ansi16_disables_line_and_gutter_backgrounds()

作用:测试 16 色模式彻底禁用整行和行号栏背景,即使主题提供了颜色也不用。

数据流:生成 16 色 fallback 和主题背景解析结果 → 检查行背景为空、亮色 gutter 只用黑色前景。

调用关系:它保护 RichDiffColorLevel::from_diff_color_level 返回 None 后的所有分支。

调用图:调用 1 个内部函数(resolve_diff_backgrounds_for);外部调用 1 个(assert_eq!)。

tests::light_truecolor_theme_uses_readable_gutter_and_line_backgrounds2020–2055 ↗
fn light_truecolor_theme_uses_readable_gutter_and_line_backgrounds()

作用:测试亮色真彩色主题下行背景和行号栏背景都清晰可读。

数据流:取新增/删除行背景和 gutter 样式 → 与预设浅色和较深 gutter 色比较。

调用关系:它保护亮色主题的 GitHub 风格配色。

调用图:外部调用 1 个(assert_eq!)。

tests::light_theme_wrapped_lines_keep_number_gutter_contrast2058–2089 ↗
fn light_theme_wrapped_lines_keep_number_gutter_contrast()

作用:测试亮色主题下,换行后的续行行号栏也保持同样的对比度。

数据流:用很窄宽度渲染一条新增行迫使换行 → 检查第一行和续行 gutter 样式、整行背景一致。

调用关系:它覆盖 push_wrapped_diff_line_inner_with_theme_and_color_level 的续行样式。

调用图:调用 3 个内部函数(fallback_diff_backgrounds, line_number_width, push_wrapped_diff_line_inner_with_theme_and_color_level);外部调用 2 个(assert!, assert_eq!)。

tests::windows_terminal_promotes_ansi16_to_truecolor_for_diffs2092–2102 ↗
fn windows_terminal_promotes_ansi16_to_truecolor_for_diffs()

作用:测试识别到 Windows Terminal 时,即使报告 16 色也提升为真彩色。

数据流:输入 Ansi16 和 WindowsTerminal → 调颜色策略 → 断言输出 TrueColor。

调用关系:它验证 diff_color_level_for_terminal 的 Windows Terminal 特例。

调用图:外部调用 1 个(assert_eq!)。

tests::wt_session_promotes_ansi16_to_truecolor_for_diffs2105–2115 ↗
fn wt_session_promotes_ansi16_to_truecolor_for_diffs()

作用:测试存在 WT_SESSION 时会把 16 色提升为真彩色。

数据流:输入 Ansi16、未知终端名、has_wt_session=true → 策略输出 TrueColor。

调用关系:它覆盖 diff_color_level_for_terminal 中 WT_SESSION 优先规则。

调用图:外部调用 1 个(assert_eq!)。

tests::non_windows_terminal_keeps_ansi16_diff_palette2118–2128 ↗
fn non_windows_terminal_keeps_ansi16_diff_palette()

作用:测试非 Windows Terminal 的 16 色终端不会被误提升。

数据流:输入 Ansi16 和 WezTerm、没有 WT_SESSION → 策略输出 Ansi16。

调用关系:它防止颜色策略过度乐观。

调用图:外部调用 1 个(assert_eq!)。

tests::wt_session_promotes_unknown_color_level_to_truecolor2131–2141 ↗
fn wt_session_promotes_unknown_color_level_to_truecolor()

作用:测试 WT_SESSION 存在时,即使原始颜色能力未知,也按真彩色处理。

数据流:输入 Unknown、WindowsTerminal、has_wt_session=true → 输出 TrueColor。

调用关系:它保护 Windows Terminal 默认体验。

调用图:外部调用 1 个(assert_eq!)。

tests::non_wt_windows_terminal_keeps_unknown_color_level_conservative2144–2154 ↗
fn non_wt_windows_terminal_keeps_unknown_color_level_conservative()

作用:测试没有 WT_SESSION 时,未知颜色能力不盲目提升。

数据流:输入 Unknown、WindowsTerminal、has_wt_session=false → 输出 Ansi16。

调用关系:它让 diff_color_level_for_terminal 在证据不足时保守。

调用图:外部调用 1 个(assert_eq!)。

tests::explicit_force_override_keeps_ansi16_on_windows_terminal2157–2167 ↗
fn explicit_force_override_keeps_ansi16_on_windows_terminal()

作用:测试用户设置 FORCE_COLOR 时,不自动把 Windows Terminal 的 16 色提升。

数据流:输入 Ansi16、WindowsTerminal、force override=true → 输出 Ansi16。

调用关系:它验证颜色策略尊重用户显式设置。

调用图:外部调用 1 个(assert_eq!)。

tests::explicit_force_override_keeps_ansi256_on_windows_terminal2170–2180 ↗
fn explicit_force_override_keeps_ansi256_on_windows_terminal()

作用:测试用户强制颜色时,WT_SESSION 也不会把 256 色提升成真彩色。

数据流:输入 Ansi256、WT_SESSION=true、force override=true → 输出 Ansi256。

调用关系:它保护 FORCE_COLOR 的优先级。

调用图:外部调用 1 个(assert_eq!)。

tests::add_diff_uses_path_extension_for_highlighting2183–2202 ↗
fn add_diff_uses_path_extension_for_highlighting()

作用:测试新增 .rs 文件会按 Rust 做语法高亮。

数据流:构造 highlight_add.rs 的 Add 变化 → 渲染摘要 → 检查至少有 RGB 前景色 span。

调用关系:它验证 render_changes_block 通过 detect_lang_for_path 把扩展名传给高亮。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 3 个(new, from, assert!)。

tests::cpp_module_extensions_use_cpp_highlighting2205–2236 ↗
fn cpp_module_extensions_use_cpp_highlighting()

作用:测试多种 C++ 模块扩展名都能触发 C++ 高亮,包括大小写混合。

数据流:循环构造不同扩展名文件 → 渲染新增内容 → 收集带 RGB 的 token → 做调试快照。

调用关系:它间接验证扩展名传递和高亮模块的语言识别。

调用图:外部调用 1 个(assert_debug_snapshot!)。

tests::unknown_extension_falls_back_without_syntax_highlighting2239–2254 ↗
fn unknown_extension_falls_back_without_syntax_highlighting()

作用:测试未知扩展名不会乱套语法高亮。

数据流:构造 unknown-extension 文件 → 渲染摘要 → 检查所有 span 都没有 RGB 前景色。

调用关系:它保护 detect_lang_for_path 与高亮失败后的安全退化。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 3 个(new, from, assert!)。

tests::delete_diff_uses_path_extension_for_highlighting2257–2276 ↗
fn delete_diff_uses_path_extension_for_highlighting()

作用:测试删除 .py 文件时也会按 Python 做语法高亮。

数据流:构造 highlight_delete.py 的 Delete 变化 → 渲染摘要 → 检查存在 RGB 前景色 span。

调用关系:它覆盖 render_change 的 Delete 分支和语言检测。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 3 个(new, from, assert!)。

tests::detect_lang_for_common_paths2279–2288 ↗
fn detect_lang_for_common_paths()

作用:测试常见带扩展名路径能检测语言,没扩展名的文件返回空。

数据流:输入 .rs、.py、.tsx、Makefile 等路径 → 调 detect_lang_for_path → 检查 Some 或 None。

调用关系:它直接覆盖语言检测小工具。

调用图:外部调用 1 个(assert!)。

tests::wrap_styled_spans_single_line2291–2296 ↗
fn wrap_styled_spans_single_line()

作用:测试短文本不需要换行时只产生一行。

数据流:输入 short span 和 80 列宽 → wrap_styled_spans → 断言结果长度为 1。

调用关系:它验证换行函数的最简单情况。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 2 个(assert_eq!, vec!)。

tests::wrap_styled_spans_splits_long_content2299–2309 ↗
fn wrap_styled_spans_splits_long_content()

作用:测试长文本会被拆成多行。

数据流:输入 100 个 a 和 40 列宽 → wrap_styled_spans → 断言至少 3 行。

调用关系:它覆盖 wrap_styled_spans 的基本拆分能力。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 2 个(assert!, vec!)。

tests::wrap_styled_spans_flushes_at_span_boundary2312–2334 ↗
fn wrap_styled_spans_flushes_at_span_boundary()

作用:测试一个 span 刚好填满一行时,下一个 span 必须另起一行。

数据流:输入红色 aaaa 和蓝色 bb,宽度 4 → 换行 → 检查结果两行且第一行不超宽。

调用关系:它防止 wrap_styled_spans 在样式片段边界处产生超宽行。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 4 个(default, assert!, assert_eq!, vec!)。

tests::wrap_styled_spans_preserves_styles2337–2348 ↗
fn wrap_styled_spans_preserves_styles()

作用:测试换行不会丢掉原来的文字样式。

数据流:输入一段绿色长文本 → wrap_styled_spans 拆行 → 检查每个拆出来的 span 仍是绿色。

调用关系:它保护语法高亮跨换行仍然保留颜色。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 3 个(default, assert_eq!, vec!)。

tests::wrap_styled_spans_tabs_have_visible_width2351–2361 ↗
fn wrap_styled_spans_tabs_have_visible_width()

作用:测试 tab 会按可见宽度算,而不是当成 0 宽。

数据流:输入 tab 加文字,宽度 8 → 换行 → 断言产生至少两行。

调用关系:它覆盖 TAB_WIDTH 在 wrap_styled_spans 中的使用。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 2 个(assert!, vec!)。

tests::wrap_styled_spans_wraps_before_first_overflowing_char2364–2390 ↗
fn wrap_styled_spans_wraps_before_first_overflowing_char()

作用:测试遇到第一个会超宽的字符时,先换行再放这个字符。

数据流:输入包含 tab 和宽字符的文本,宽度 5 → 换行 → 检查行内容和每行宽度。

调用关系:它保护 wrap_styled_spans 对 tab、中文等宽字符的边界处理。

调用图:调用 1 个内部函数(wrap_styled_spans);外部调用 3 个(assert!, assert_eq!, vec!)。

tests::fallback_wrapping_uses_display_width_for_tabs_and_wide_chars2393–2411 ↗
fn fallback_wrapping_uses_display_width_for_tabs_and_wide_chars()

作用:测试普通 diff 行换行时也按真实显示宽度计算,包括 tab、中文和 emoji。

数据流:渲染包含复杂字符的新增行 → 检查输出多行且每行显示宽度不超过限制。

调用关系:它从公开包装函数一路覆盖到底层 wrap_styled_spans。

调用图:调用 3 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_with_style_context);外部调用 1 个(assert!)。

tests::large_update_diff_skips_highlighting2414–2465 ↗
fn large_update_diff_skips_highlighting()

作用:测试超大 diff 会跳过语法高亮,避免渲染卡住。

数据流:构造超过限制的大 patch → create_diff_summary 渲染 → 检查有大量输出,但没有 RGB 高亮 span。

调用关系:它验证 render_change 中 exceeds_highlight_limits 的保护逻辑。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 5 个(new, from, assert!, create_patch, panic!)。

tests::rename_diff_uses_destination_extension_for_highlighting2468–2495 ↗
fn rename_diff_uses_destination_extension_for_highlighting()

作用:测试重命名时按新文件扩展名选择高亮语言。比如从未知扩展改名为 .rs,就应该按 Rust 高亮。

数据流:构造 foo.xyzzy 到 foo.rs 的 Update → 渲染摘要 → 检查存在 RGB 高亮 span。

调用关系:它保护 render_changes_block 中 move_path 优先用于语言检测的规则。

调用图:调用 1 个内部函数(create_diff_summary);外部调用 4 个(new, from, assert!, create_patch)。

tests::update_diff_preserves_multiline_highlight_state_within_hunk2498–2533 ↗
fn update_diff_preserves_multiline_highlight_state_within_hunk()

作用:测试修改 diff 的同一个 hunk 内,多行字符串这类语法状态能连续保留。

数据流:构造 Rust 多行字符串改动 → 单独算预期高亮样式 → 渲染 diff 后找 world 的样式 → 断言两者一致。

调用关系:它验证 render_change 按 hunk 整块高亮,而不是逐行高亮导致语法状态丢失。

调用图:调用 2 个内部函数(create_diff_summary, highlight_code_to_styled_spans);外部调用 4 个(new, from, assert_eq!, create_patch)。

tui/src/git_action_directives.rs源码 ↗
domain_logicrequest handling / transcript rendering

助手有时会在 Markdown 里夹一些给程序看的小纸条,比如“把这个仓库改动暂存”“推送这个分支”“在某个文件某几行留一条评论”。这些内容不该原样显示给用户,否则界面会出现一堆奇怪的 ::git-... 文本。这个文件就像一个收信分拣员:逐行读助手的回复,把普通文字留下;把 Git 指令从显示文本里拿掉,变成 GitActionDirective 这种明确的动作;把 code-comment 指令改写成用户能看懂的项目符号列表。它还会去重 Git 动作,避免同一个隐藏指令重复执行;会去掉末尾多余空行;遇到格式坏掉的指令时,尽量不把它变成错误动作。重要的是,这里并不真的执行 Git,它只是把“文字里的命令”整理成“程序可执行的计划”。

函数细节16
GitActionDirective::created_branch_cwd32–37 ↗
fn created_branch_cwd(&self) -> Option<&str>

作用:判断一个 Git 动作是不是“创建分支”,如果是,就拿出它所在的工作目录。别人用它来快速找出创建分支发生在哪个仓库目录。

数据流:进去的是一个 GitActionDirective。函数查看它的种类:如果是 CreateBranch,就返回里面的 cwd;如果是暂存、提交、推送、创建 PR 等别的动作,就返回空。它不修改任何东西。

调用关系:它是 GitActionDirective 自己的小工具。ParsedAssistantMarkdown::last_created_branch_cwd 会倒着扫描动作列表,并把每个动作交给它判断,从而找到最后一次创建分支的目录。

ParsedAssistantMarkdown::last_created_branch_cwd47–52 ↗
fn last_created_branch_cwd(&self) -> Option<&str>

作用:从已经解析好的助手回复里,找出最后一个“创建分支”动作所在的目录。这样后续界面或流程可以知道最近创建分支的是哪个仓库。

数据流:进去的是 ParsedAssistantMarkdown,里面有可见 Markdown 和 Git 动作列表。函数从动作列表末尾往前看,每个动作都问 GitActionDirective::created_branch_cwd 是否有目录;找到第一个就返回,找不到就返回空。它不改变原数据。

调用关系:它用在需要知道“最后一次创建分支发生在哪里”的地方。测试 tests::last_created_branch_cwd_uses_the_last_matching_directive 会先用 parse_assistant_markdown 解析文本,再调用它确认拿到的是最后那个目录。

parse_assistant_markdown55–85 ↗
fn parse_assistant_markdown(markdown: &str, cwd: &Path) -> ParsedAssistantMarkdown

作用:这是本文件的主入口:把助手 Markdown 拆成两份,一份是用户能看到的干净文字,另一份是程序要记住的 Git 动作。界面展示助手回复时会用它,避免把隐藏指令直接露出来。

数据流:进去的是一整段 Markdown 字符串和当前工作目录 cwd。函数逐行读取:先尝试用 rewrite_code_comment_line 把代码评论指令改写成可见列表;如果不是代码评论,再用 strip_line_directives 把行内 Git 指令剥掉并收集动作。它用一个集合记住已经见过的动作,防止重复加入。最后删除可见文本末尾的空行,出来一个 ParsedAssistantMarkdown,里面有 visible_markdown 和 git_actions。

调用关系:它是外部最常调用的解析器,被 thread_to_transcript_cells 在生成对话显示内容时使用,也被多个测试直接验证。它把具体小活分给 rewrite_code_comment_line 和 strip_line_directives;后者再继续分给更底层的属性解析函数。

调用图:调用 2 个内部函数(rewrite_code_comment_line, strip_line_directives);被 6 处调用(hides_malformed_directives_without_materializing_rows, last_created_branch_cwd_uses_the_last_matching_directive, preserves_non_directive_and_malformed_code_comment_text, renders_code_comment_directives_as_markdown, strips_and_parses_git_action_directives, thread_to_transcript_cells);外部调用 2 个(new, new)。

rewrite_code_comment_line87–132 ↗
fn rewrite_code_comment_line(line: &str, cwd: &Path) -> Option<String>

作用:把一整行 code-comment 指令改写成普通 Markdown 列表项。这样“在某文件某行留下评论”的机器指令,可以变成用户能读懂的一条建议。

数据流:进去的是一行文本和当前工作目录 cwd。函数先看这一行是不是以 1 到 3 个冒号开头的 code-comment 指令;不是就返回空。是的话,它解析 title、body、file、start、end、priority 等属性,检查标题、正文、文件是否为空;再把绝对路径尽量改成相对 cwd 的路径,把反斜杠换成斜杠。最后输出一段形如“- 标题 — 文件:行号\n 正文”的 Markdown 文本。原输入不被修改。

调用关系:parse_assistant_markdown 每读一行都会先找它帮忙。它内部会调用 parse_code_comment_attributes 读取属性,调用 directive_integer 读数字行号和优先级,调用 title_has_priority 判断标题里是否已经写了优先级。

调用图:调用 3 个内部函数(directive_integer, parse_code_comment_attributes, title_has_priority);被 1 处调用(parse_assistant_markdown);外部调用 2 个(new, format!)。

strip_line_directives134–160 ↗
fn strip_line_directives(line: &str) -> (String, Vec<GitActionDirective>)

作用:从一行文字里拿掉 ::git-... 这种隐藏 Git 指令,同时把这些指令变成明确的动作对象。这样用户看到的是正常句子,程序拿到的是可执行计划。

数据流:进去的是一行文本。函数从左到右寻找“::git-”,把指令前面的普通文字加入可见文本;如果找到完整的大括号内容,就把指令名和属性交给 parse_git_action。能解析成动作的就放进动作列表,解析不了的就不加入动作。遇到缺少左括号或右括号的坏格式时,它会把剩余文字保留下来并结束。出来的是“去掉指令后的可见行”和“这一行发现的 Git 动作列表”。

调用关系:parse_assistant_markdown 在确认一行不是 code-comment 改写行后,会调用它。它自己不懂每种 Git 动作怎么构造,而是把这件事交给 parse_git_action。

调用图:调用 1 个内部函数(parse_git_action);被 1 处调用(parse_assistant_markdown);外部调用 2 个(new, new)。

directive_integer162–169 ↗
fn directive_integer(attributes: &HashMap<String, String>, name: &str) -> Option<i64>

作用:从一组属性里取出某个数字值,比如行号 start、end,或者优先级 priority。它还允许数字前面带 P 或 p,方便解析像 P2 这样的写法。

数据流:进去的是属性表和属性名。函数找到对应字符串后,先去掉前后空白,再去掉开头的 P 或 p,然后尝试转成整数。成功就返回数字,失败或没有这个属性就返回空。它不修改属性表。

调用关系:rewrite_code_comment_line 用它读取 start、end、priority。这样 rewrite_code_comment_line 可以专心组织显示文本,不用自己写数字清洗规则。

调用图:被 1 处调用(rewrite_code_comment_line)。

title_has_priority171–178 ↗
fn title_has_priority(title: &str) -> bool

作用:检查标题开头是否已经有类似 [P1] 的优先级标记。这样系统不会重复加出 “[P2] [P1] 标题” 这种难看的文字。

数据流:进去的是标题字符串。函数跳过开头空白后,看前四个字节是不是左方括号、P 或 p、一个数字、右方括号。符合就返回 true,否则返回 false。它只读取文本,不改文本。

调用关系:rewrite_code_comment_line 在准备 code-comment 的标题时调用它。如果标题已经带优先级,就直接使用;如果没有,再考虑根据 priority 属性补上 [P数字]。

调用图:被 1 处调用(rewrite_code_comment_line);外部调用 1 个(matches!)。

parse_code_comment_attributes180–200 ↗
fn parse_code_comment_attributes(input: &str) -> Option<HashMap<String, String>>

作用:解析 code-comment 指令大括号里的属性,比如 title="..." body="..." file="..."。它比普通 Git 属性解析更细心,支持引号里的转义双引号。

数据流:进去的是一段属性文本。函数从左到右找 key=value:key 不能为空;value 可以是双引号包住的长文本,也可以是不带空格的短文本。遇到带引号的值时,它交给 parse_quoted_value 读取,支持 \" 这种把双引号当内容的写法。解析成功出来一个属性表;格式不对就返回空。

调用关系:rewrite_code_comment_line 靠它把 code-comment 的原始字符串变成可查询的属性表。它进一步调用 parse_quoted_value,专门解决带引号长文本的读取问题。

调用图:调用 1 个内部函数(parse_quoted_value);被 1 处调用(rewrite_code_comment_line);外部调用 1 个(new)。

parse_git_action202–224 ↗
fn parse_git_action(name: &str, attributes: &str) -> Option<GitActionDirective>

作用:把一个 Git 指令名和它的属性,翻译成程序内部认识的 GitActionDirective。比如把 git-push 加 cwd 和 branch 变成“推送某分支”的动作。

数据流:进去的是指令名,比如 git-stage、git-push,以及大括号里的属性字符串。函数先用 parse_attributes 得到属性表,再要求必须有 cwd;不同指令再要求 branch、url、isDraft 等需要的字段。匹配成功就返回对应的 GitActionDirective;缺字段或指令名不认识就返回空。

调用关系:strip_line_directives 找到一个 ::git-... 指令后,会把名字和属性交给它。它不负责扫描整行,只负责把单个指令翻译成动作。

调用图:调用 1 个内部函数(parse_attributes);被 1 处调用(strip_line_directives)。

parse_attributes226–247 ↗
fn parse_attributes(input: &str) -> Option<std::collections::HashMap<String, String>>

作用:解析 Git 指令里的简单 key=value 属性。它负责把 cwd="/repo" branch="feat/x" 这种文本变成方便查询的表。

数据流:进去的是属性字符串。函数逐段寻找等号,拿等号左边当 key,右边当 value;value 可以用双引号包住,也可以是不带空格的裸值。成功时输出一个键值表;如果缺等号、key 为空、引号没闭合,就返回空。它不理解 Git 含义,只做基础拆分。

调用关系:parse_git_action 调用它来得到属性表,然后再决定这些属性能不能组成某个 GitActionDirective。它是 Git 指令解析的底层小零件。

调用图:被 1 处调用(parse_git_action);外部调用 1 个(new)。

parse_quoted_value249–267 ↗
fn parse_quoted_value(input: &str) -> Option<(String, &str)>

作用:读取一个双引号字符串的内容,并正确处理里面被转义的双引号。简单说,它知道 \" 应该算作内容里的一个引号,而不是字符串结束。

数据流:进去的是已经去掉开头双引号之后的文本。函数一个字符一个字符往后读:遇到普通字符就加入结果;遇到反斜杠加双引号,就把双引号放进结果并跳过转义;遇到未转义的双引号,就返回读到的值和剩下还没读的文本。如果一直没找到结束引号,就返回空。

调用关系:parse_code_comment_attributes 在读取 code-comment 的带引号属性值时调用它。这样 body 里可以安全包含像 role=\"tab\" 这样的内容,不会把解析弄乱。

调用图:被 1 处调用(parse_code_comment_attributes);外部调用 1 个(new)。

tests::strips_and_parses_git_action_directives275–297 ↗
fn strips_and_parses_git_action_directives()

作用:这个测试确认 Git 隐藏指令会从可见 Markdown 里消失,同时会被正确收集成动作列表。它也验证重复动作不会被随便丢错,Windows 路径也能保留下来。

数据流:进去的是一段包含 Done 和多个 ::git-... 指令的测试 Markdown。测试调用 parse_assistant_markdown,得到可见文本和动作列表;然后用断言检查可见文本只剩 Done,动作列表包含暂存、推送、另一个暂存等预期结果。

调用关系:它直接测试 parse_assistant_markdown 的主流程。通过这个测试,可以发现 strip_line_directives、parse_git_action、parse_attributes 任一环节坏掉后带来的结果偏差。

调用图:调用 1 个内部函数(parse_assistant_markdown);外部调用 2 个(new, assert_eq!)。

tests::hides_malformed_directives_without_materializing_rows300–305 ↗
fn hides_malformed_directives_without_materializing_rows()

作用:这个测试确认格式不完整的 Git 指令不会变成假动作,也不会在界面里留下多余空行。它防止坏指令污染用户看到的内容或后续 Git 流程。

数据流:进去的是一段有 ::git-push 但缺少 branch 的 Markdown。测试调用 parse_assistant_markdown 后,检查可见文本是 Done,并且动作列表为空。

调用关系:它验证 parse_assistant_markdown 与 strip_line_directives、parse_git_action 的配合:指令外壳会被剥掉,但因为属性不够,parse_git_action 不会生成动作。

调用图:调用 1 个内部函数(parse_assistant_markdown);外部调用 3 个(new, assert!, assert_eq!)。

tests::renders_code_comment_directives_as_markdown308–321 ↗
fn renders_code_comment_directives_as_markdown()

作用:这个测试确认 code-comment 指令会被改写成普通 Markdown,而不是被当作 Git 指令或原样显示。它还覆盖了优先级、文件路径、行号范围和带转义引号的正文。

数据流:进去的是一段包含两个 code-comment 指令的 Markdown。测试调用 parse_assistant_markdown,拿到可见 Markdown 后用快照断言检查渲染结果,并确认 Git 动作列表为空。

调用关系:它主要覆盖 rewrite_code_comment_line 的行为,同时也经过 parse_assistant_markdown 的逐行入口。间接检查 parse_code_comment_attributes、parse_quoted_value、directive_integer、title_has_priority 是否一起工作正常。

调用图:调用 1 个内部函数(parse_assistant_markdown);外部调用 4 个(new, assert!, concat!, assert_snapshot!)。

tests::preserves_non_directive_and_malformed_code_comment_text324–329 ↗
fn preserves_non_directive_and_malformed_code_comment_text()

作用:这个测试确认普通提到 code-comment 的文字,或者缺少必要字段的 code-comment,不会被误改写。它保护的是“别太激进解析”的安全边界。

数据流:进去的是一段包含内联 code-comment 示例和一个缺 body 的 code-comment 行的 Markdown。测试调用 parse_assistant_markdown,然后确认输出的 visible_markdown 和原文完全一样。

调用关系:它检查 parse_assistant_markdown 调 rewrite_code_comment_line 时的保守行为:只有完整、合法、在行首带指定冒号标记的 code-comment 才会改写。

调用图:调用 1 个内部函数(parse_assistant_markdown);外部调用 2 个(new, assert_eq!)。

tests::last_created_branch_cwd_uses_the_last_matching_directive332–339 ↗
fn last_created_branch_cwd_uses_the_last_matching_directive()

作用:这个测试确认如果助手回复里有多个创建分支动作,系统会取最后一个的目录。这样后续逻辑不会误用前面已经过时的分支创建位置。

数据流:进去的是一段包含“创建 first 分支”“推送”“创建 second 分支”的 Markdown。测试先调用 parse_assistant_markdown 得到动作列表,再调用 last_created_branch_cwd,最后断言结果是 /second。

调用关系:它把 parse_assistant_markdown、ParsedAssistantMarkdown::last_created_branch_cwd 和 GitActionDirective::created_branch_cwd 串起来测。这个测试证明倒序查找创建分支动作的流程是对的。

调用图:调用 1 个内部函数(parse_assistant_markdown);外部调用 2 个(new, assert_eq!)。

tui/src/bottom_pane/mentions_v2/render.rs源码 ↗
domain_logicmain loop / popup rendering

这个文件专门负责渲染提及候选框,也就是用户输入类似“@”后看到的下方小弹窗。它先把弹窗区域切成两块:上面放搜索结果,下面可选地放操作提示。结果列表会根据滚动位置和当前选中项决定从哪一行开始画,保证选中的项尽量出现在可见范围内。每条结果会被拼成一行:左边是主要名字,中间可能有路径或说明,右边贴一个类型标签。文件名、插件、技能等会用不同颜色或明暗区分;匹配到的字符会加粗,像搜索软件里高亮命中的字一样。内容太长时会用省略号截断,避免挤坏终端布局。整体上,它就像一个“排版工”,把候选数据整理成用户能快速扫读的终端界面。

函数细节10
render_popup23–68 ↗
fn render_popup(
    area: Rect,
    buf: &mut Buffer,
    rows: &[SearchResult],
    state: &ScrollState,
    empty_message: &str,
    search_mode: SearchMode,
)

作用:这是画整个提及弹窗的入口。它决定哪里画结果列表,哪里画底部提示,并把具体绘制工作分给下面的小函数。

数据流:进去的是一块终端区域、要写入的画布、搜索结果、滚动状态、空结果提示文字和搜索模式。它先判断空间够不够放底部提示,再把区域切成列表区和提示区;列表区会缩进一点后交给 render_rows,提示区交给 render_footer。出来的结果不是返回值,而是终端画布 Buffer 被写上了弹窗内容。

调用关系:它由 render_ref 在需要显示提及弹窗时调用。它自己不逐字排版,而是把列表交给 render_rows,把底部操作提示交给 render_footer,并用 tlbr 生成缩进用的边距。

调用图:调用 3 个内部函数(render_footer, render_rows, tlbr);被 1 处调用(render_ref)。

render_rows70–126 ↗
fn render_rows(
    area: Rect,
    buf: &mut Buffer,
    rows: &[SearchResult],
    state: &ScrollState,
    empty_message: &str,
)

作用:这个函数负责把候选结果一行一行画出来。它还会处理空列表、滚动位置和当前选中项,保证用户看到的是该看的那几行。

数据流:进去的是列表区域、画布、所有搜索结果、滚动状态和空结果提示。如果区域高度为 0,它什么也不做;如果没有结果,它画一行斜体提示。否则它算出最多能显示多少行,并调整起始位置,让选中项留在可见范围内;接着逐条调用 build_line 做出单行内容,再写进 Buffer。

调用关系:它只被 render_popup 调用,是弹窗上半部分的主要执行者。它把每条结果的具体排版交给 build_line,自己专注于“显示哪几条”和“画到第几行”。

调用图:调用 1 个内部函数(build_line);被 1 处调用(render_popup);外部调用 4 个(from, is_empty, iter, len)。

build_line128–161 ↗
fn build_line(
    row: &SearchResult,
    selected: bool,
    width: usize,
    primary_column_width: usize,
) -> Line<'static>

作用:这个函数把一条搜索结果变成终端里的一整行文字。它会决定选中时加粗、右侧类型标签、以及内容太长时如何截断。

数据流:进去的是一条结果、是否被选中、可用宽度和主列宽度。它先准备普通样式和弱化样式,再拿到右侧类型标签;剩余宽度用来放主要内容,并通过 truncate_line_with_ellipsis_if_overflow 加省略号防止溢出。最后它补空格,把类型标签推到右边,产出一个 Line。

调用关系:它由 render_rows 对每个可见候选项调用。它会找 content_line 生成左侧正文,再调用截断工具保证正文不超宽,最后把正文和类型标签拼成最终一行。

调用图:调用 2 个内部函数(content_line, truncate_line_with_ellipsis_if_overflow);被 1 处调用(render_rows);外部调用 3 个(from, default, new)。

content_line163–180 ↗
fn content_line(
    row: &SearchResult,
    base_style: Style,
    dim_style: Style,
    primary_column_width: usize,
) -> Line<'static>

作用:这个函数负责组装一条结果的“正文部分”。正文通常包含主要名字,后面可能跟路径、说明文字等次要信息。

数据流:进去的是搜索结果、普通样式、弱化样式和主列宽度。它先用 primary_spans 做出主要名字;如果 secondary_line 能提供次要信息,就根据主列宽度补空格对齐,再把次要信息接上。最后输出一个 Line,供 build_line 放进整行里。

调用关系:它被 build_line 调用,是一行里左侧内容的装配点。它向 primary_spans 要主标题,向 secondary_line 要副标题,并用 primary_text_width 算对齐用的宽度。

调用图:调用 3 个内部函数(primary_spans, primary_text_width, secondary_line);被 1 处调用(build_line);外部调用 2 个(from, new)。

primary_spans182–213 ↗
fn primary_spans(row: &SearchResult, base_style: Style) -> Vec<Span<'static>>

作用:这个函数生成候选项最重要的那段文字,比如文件名、插件名或技能名。它还会根据类型上色,并把搜索命中的字符加粗。

数据流:进去的是一条搜索结果和基础样式。它先判断这条结果能不能拆出文件名;如果能,并且是文件类型,就把文件名染成青色。否则它按提及类型选择颜色或明暗:插件偏紫色,技能变淡,普通文件或目录保持基础样式;如果有匹配字符位置,就逐字检查并加粗命中的字符。出来的是一组 Span,也就是带样式的小段文字。

调用关系:它由 content_line 调用,用来提供正文的主列。它会调用 file_name 判断是否该只显示路径里的最后一段文件名。

调用图:调用 1 个内部函数(file_name);被 1 处调用(content_line);外部调用 5 个(dim, fg, magenta, with_capacity, vec!)。

secondary_line215–237 ↗
fn secondary_line(
    row: &SearchResult,
    base_style: Style,
    dim_style: Style,
) -> Option<Line<'static>>

作用:这个函数生成候选项的次要说明。对文件来说,它通常显示路径和描述;对其他项来说,它显示描述文字。

数据流:进去的是搜索结果、基础样式和弱化样式。它先看这条结果是否有文件名;如果有,就用 path_spans 做出路径部分,再把非空描述接在后面。若不是文件路径类结果,它只在描述存在且不为空时生成一行弱化文字。没有可显示内容时返回 None。

调用关系:它由 content_line 调用,负责主名字后面的补充信息。它会再次用 file_name 判断文件类结果,并把路径绘制细节交给 path_spans。

调用图:调用 2 个内部函数(file_name, path_spans);被 1 处调用(content_line);外部调用 1 个(from)。

path_spans239–271 ↗
fn path_spans(row: &SearchResult, base_style: Style) -> Vec<Span<'static>>

作用:这个函数把文件路径部分做成带样式的文字。它会让路径变暗,把视觉重点留给文件名,同时也能高亮搜索命中的路径字符。

数据流:进去的是一条搜索结果和基础样式。它先用 file_name_start 找到文件名从第几个字符开始;如果文件就在当前目录,就显示“./”;如果有匹配字符位置,就逐字画出文件名前面的路径并加粗命中字符;如果没有匹配信息,就直接取路径前缀。出来的是一组表示路径的 Span。

调用关系:它由 secondary_line 在需要显示文件路径时调用。它依赖 file_name_start 判断哪部分是路径、哪部分是文件名。

调用图:调用 1 个内部函数(file_name_start);被 1 处调用(secondary_line);外部调用 2 个(dim, with_capacity)。

primary_text_width273–277 ↗
fn primary_text_width(row: &SearchResult) -> usize

作用:这个函数计算主标题占多少字符宽度。它主要用于让多行候选项的次要说明能对齐,看起来像表格一样整齐。

数据流:进去的是一条搜索结果。它先尝试通过 file_name 取出文件名;如果能取到,就数文件名字符数;否则数 display_name 的字符数。出来的是一个宽度数字。

调用关系:它被 content_line 用来补空格对齐,也被 render_rows 用来计算当前可见列表中主列的最大宽度。它把“文件路径只按文件名算宽度”的判断交给 file_name。

调用图:调用 1 个内部函数(file_name);被 1 处调用(content_line)。

file_name279–295 ↗
fn file_name(row: &SearchResult) -> Option<&str>

作用:这个函数从显示名称里切出真正的文件名。比如把“src/main.rs”变成“main.rs”,方便界面突出最重要的部分。

数据流:进去的是一条搜索结果。它先调用 file_name_start 找文件名起点;如果结果表示不能当文件路径处理,就返回 None;如果起点是 0,就说明整个名字都是文件名;否则它按字符位置换算成字节位置,再切出后半段字符串。出来的是可选的文件名引用。

调用关系:它被 primary_spans、primary_text_width 和 secondary_line 使用,是判断“这条结果是否适合按文件路径展示”的公共小工具。它把起点判断交给 file_name_start。

调用图:调用 1 个内部函数(file_name_start);被 3 处调用(primary_spans, primary_text_width, secondary_line)。

file_name_start297–306 ↗
fn file_name_start(row: &SearchResult) -> usize

作用:这个函数判断文件名在整条路径里从哪个字符开始。它也会决定某些结果不能按文件路径来拆。

数据流:进去的是一条搜索结果。它检查 selection 和 mention_type:只有真正的文件系统项才会去找最后一个斜杠或反斜杠;找到了就返回斜杠后面的字符位置,没找到就返回 0。对于不是文件系统路径的文件选择或工具选择,它返回 usize::MAX,表示“不适合拆文件名”。

调用关系:它被 file_name 和 path_spans 调用,是文件名拆分逻辑的源头。上层函数靠它统一判断路径怎么切,避免各处各自猜测。

调用图:被 2 处调用(file_name, path_spans)。

tui/src/bottom_pane/request_user_input/layout.rs源码 ↗
domain_logicrendering

这个文件解决的是终端界面空间不固定的问题。终端用户可能把窗口拉得很小,但提问弹窗里可能同时有进度行、问题文字、选项列表、备注输入框和底部提示。这里的代码就像给一个小盒子分隔间:先看有没有选项,再看备注要不要显示,然后计算每一块能分到多少高度。如果空间够,就尽量显示完整;如果空间不够,就优先保住最重要的内容,比如问题和至少一点选项区域,再压缩备注、提示或空行;极端情况下还会截断问题行数。最后,它把这些“高度计划”变成真正的矩形区域(Rect,也就是屏幕上的一块位置和大小),供后面的绘制代码直接使用。这样绘制代码不用自己猜哪里该画什么,只要按这里算好的区域画就行。

函数细节7
RequestUserInputOverlay::layout_sections19–60 ↗
fn layout_sections(&self, area: Rect) -> LayoutSections

作用:这是这个文件的总入口,用来把一整块可用屏幕区域切成几个小区域。外层绘制提问弹窗时会先问它:“现在窗口这么大,各部分该画在哪里?”

数据流:输入是一块可用区域 area,以及它从当前弹窗对象里读取到的信息,比如有没有选项、备注是否可见、问题文字换行后有几行、底部提示需要几行。它先决定走“有选项”的排版方案还是“无选项”的排版方案,再把算出的高度交给区域生成步骤。输出是 LayoutSections,里面包含进度区、问题区、选项区、备注区、换行后的问题文字,以及底部提示行数。

调用关系:它是排版计算的调度点。它根据是否有选项,分别把工作交给 RequestUserInputOverlay::layout_with_options 或 RequestUserInputOverlay::layout_without_options;拿到高度安排后,再交给 RequestUserInputOverlay::build_layout_areas 变成真正能画的屏幕矩形。

调用图:调用 3 个内部函数(build_layout_areas, layout_with_options, layout_without_options)。

RequestUserInputOverlay::layout_with_options63–95 ↗
fn layout_with_options(
        &self,
        args: OptionsLayoutArgs,
        question_lines: &mut Vec<String>,
    ) -> LayoutPlan

作用:这个函数处理“问题下面有可选答案”的情况。它会先保证选项区域至少有一点位置,不让选项被问题文字完全挤没。

数据流:输入是一组排版参数,包括总高度、宽度、问题高度、备注理想高度、底部提示理想高度,以及备注是否显示;另外还拿到问题行列表。它会先看问题文字是不是太高,如果太高,就把问题高度压到能给选项留至少一行的位置,并截短问题行列表。之后它把调整后的数据继续交给正常的有选项排版流程。输出是 LayoutPlan,也就是各部分应该占几行的计划。

调用关系:它由 RequestUserInputOverlay::layout_sections 在检测到有选项时调用。它自己不做完整分配,而是先做“保底检查”,然后把主要计算交给 RequestUserInputOverlay::layout_with_options_normal。

调用图:调用 1 个内部函数(layout_with_options_normal);被 1 处调用(layout_sections)。

RequestUserInputOverlay::layout_with_options_normal99–196 ↗
fn layout_with_options_normal(
        &self,
        args: OptionsNormalArgs,
        options: OptionsHeights,
    ) -> LayoutPlan

作用:这个函数是真正给“有选项”的界面分配高度的地方。它会在问题、选项、进度、备注、底部提示和空白间隔之间做取舍。

数据流:输入是可用高度、问题高度、备注理想高度、底部提示理想高度、备注是否显示,以及选项的理想高度和完整高度。它先给选项一个尽量合适的高度,再看剩余空间够不够放进度行、底部提示和空行;如果不够,就会压缩选项。备注隐藏时,它会更偏向保留进度、提示和间隔;备注显示时,它会把空间分给备注输入区。输出是一个 LayoutPlan,说明每一块最终占几行。

调用关系:它只被 RequestUserInputOverlay::layout_with_options 调用,是“有选项”分支里的核心计算器。它不再调用别的排版函数,而是直接产出高度计划,之后由上层拿去生成屏幕区域。

调用图:被 1 处调用(layout_with_options)。

RequestUserInputOverlay::layout_without_options203–222 ↗
fn layout_without_options(
        &self,
        available_height: u16,
        question_height: u16,
        notes_pref_height: u16,
        footer_pref: u16,
        question_lines: &mut Vec<String

作用:这个函数处理“没有选项,用户主要看问题并可能输入备注”的情况。它会判断空间是够用还是太紧,然后选择不同的排版策略。

数据流:输入是总高度、问题高度、备注理想高度、底部提示理想高度,以及问题行列表。它先判断光是问题文字是否已经超过可用高度:如果超过,就进入紧凑模式,截断问题;如果没超过,就进入正常模式,继续安排备注、提示和进度。输出是 LayoutPlan。

调用关系:它由 RequestUserInputOverlay::layout_sections 在没有选项时调用。它像一个分岔路口:空间不足时交给 RequestUserInputOverlay::layout_without_options_tight,空间足够时交给 RequestUserInputOverlay::layout_without_options_normal。

调用图:调用 2 个内部函数(layout_without_options_normal, layout_without_options_tight);被 1 处调用(layout_sections)。

RequestUserInputOverlay::layout_without_options_tight225–244 ↗
fn layout_without_options_tight(
        &self,
        available_height: u16,
        question_height: u16,
        question_lines: &mut Vec<String>,
    ) -> LayoutPlan

作用:这个函数处理最窄巴的情况:没有选项,而且连问题文字都放不下。它的目标是尽量显示问题,其他东西都先不显示。

数据流:输入是可用高度、原本的问题高度,以及问题行列表。它把问题高度压到不超过可用高度,并把多出来的问题行从列表里截掉。输出的 LayoutPlan 只给问题区分配高度,进度、选项、备注和底部提示都为 0。

调用关系:它只在 RequestUserInputOverlay::layout_without_options 判断空间太小时被调用。它是兜底方案,保证界面再小也不会把区域算到超出屏幕。

调用图:被 1 处调用(layout_without_options)。

RequestUserInputOverlay::layout_without_options_normal247–279 ↗
fn layout_without_options_normal(
        &self,
        available_height: u16,
        question_height: u16,
        notes_pref_height: u16,
        footer_pref: u16,
    ) -> LayoutPlan

作用:这个函数处理没有选项且空间还算够的正常情况。它会先放问题,再放备注、底部提示和进度行。

数据流:输入是可用高度、问题高度、备注理想高度和底部提示理想高度。它先扣掉问题占用的空间,再给备注分配尽量接近理想值的高度,然后给底部提示分配空间,再如果还有位置就放一行进度。最后剩下的多余空间会加到备注区里。输出是各部分高度组成的 LayoutPlan。

调用关系:它只被 RequestUserInputOverlay::layout_without_options 在空间足够时调用。它产出的高度计划会一路返回给 RequestUserInputOverlay::layout_sections,再由后者交给区域构建函数。

调用图:被 1 处调用(layout_without_options)。

RequestUserInputOverlay::build_layout_areas282–326 ↗
fn build_layout_areas(
        &self,
        area: Rect,
        heights: LayoutPlan,
    ) -> (
        Rect, // progress_area
        Rect, // question_area
        Rect, // options_area
        Re

作用:这个函数把前面算出的“每块占几行”变成真正的屏幕矩形。简单说,前面只是说蛋糕切多厚,这里决定每块蛋糕放在盒子的哪个位置。

数据流:输入是一整块可用区域 area 和一个 LayoutPlan。它从区域顶部开始,用一个向下移动的 cursor_y 逐块摆放:先放进度区,再放问题区,再跳过问题后的空行,放选项区,再跳过选项后的空行,最后放备注区。输出是四个 Rect,分别对应进度、问题、选项和备注的实际位置与大小。

调用关系:它由 RequestUserInputOverlay::layout_sections 在所有高度都算好之后调用。它不决定取舍,只负责把高度计划落到具体坐标上,让后续绘制代码可以直接按这些矩形画界面。

调用图:被 1 处调用(layout_sections)。

tui/src/status_indicator_widget.rs源码 ↗
domain_logicmain loop / task running render

当智能体正在执行任务时,如果界面什么都不显示,用户很容易以为程序卡住了。这个文件就是为了解决这个问题:它提供一个 StatusIndicatorWidget,小小一行里放下动画指示、标题、运行时间、打断快捷键和额外消息;如果还有细节说明,就在下面用最多几行显示。它还自己记时间,支持暂停和恢复,避免把等待用户输入的时间也算进去。渲染时,它会根据终端宽度自动截断或换行,太长就加省略号,防止把底部区域撑得乱跳。可以把它理解成电梯里的“运行中”指示灯:不做核心工作,但让人放心,也提供紧急停止按钮。

函数细节29
fmt_elapsed_compact65–78 ↗
fn fmt_elapsed_compact(elapsed_secs: u64) -> String

作用:把已经过去的秒数变成适合显示在状态栏里的短文字,比如“59s”“1m 00s”“2h 03m 09s”。这样用户不用自己换算时间。

数据流:进去的是一个秒数 → 函数按小于一分钟、小于一小时、一小时以上三种情况拆成秒、分秒或时分秒 → 出来的是一段紧凑的时间字符串,不改动任何外部状态。

调用关系:它是渲染状态行时的小帮手。StatusIndicatorWidget::render 会先算出任务跑了多久,再交给它格式化,最后把结果放进界面文字里。

调用图:被 1 处调用(render);外部调用 1 个(format!)。

StatusIndicatorWidget::new81–101 ↗
fn new(
        app_event_tx: AppEventSender,
        frame_requester: FrameRequester,
        animations_enabled: bool,
    ) -> Self

作用:创建一个新的状态提示组件,并填好默认值。默认标题是“Working”,默认显示 Esc 可打断,计时从创建时开始。

数据流:进去的是发送应用事件的通道、请求重画界面的工具,以及是否开启动画的开关 → 它把这些保存起来,并初始化标题、计时器、快捷键提示等字段 → 出来的是一个可以马上渲染的 StatusIndicatorWidget。

调用关系:当界面需要开始显示“正在工作”状态时,上层流程会调用它,比如 ensure_status_indicator 或 set_task_running。测试里也大量用它造一个干净的组件来检查显示效果。

调用图:调用 1 个内部函数(plain);被 10 处调用(ensure_status_indicator, set_task_running, details_args_can_disable_capitalization_and_limit_lines, details_overflow_adds_ellipsis, renders_remapped_interrupt_hint, renders_truncated, renders_with_working_header, renders_without_spinner_when_animations_disabled, renders_wrapped_details_panama_two_lines, timer_pauses_when_requested);外部调用 2 个(now, from)。

StatusIndicatorWidget::interrupt103–106 ↗
fn interrupt(&self)

作用:在用户想中断当前任务时,向应用发出“请停止并尽量恢复输入框”的请求。

数据流:进去的是当前组件里保存的 AppEventSender → 它通过这个发送器发出中断信号 → 结果是应用主流程收到打断请求,组件本身不直接停止任务。

调用关系:它像状态栏上的“停止按钮”背后的电线。用户按下对应快捷键后,界面层会调用它,它再把请求交给 AppEventSender::interrupt_and_restore_prompt_if_no_output。

调用图:调用 1 个内部函数(interrupt_and_restore_prompt_if_no_output)。

StatusIndicatorWidget::update_header109–111 ↗
fn update_header(&mut self, header: String)

作用:改掉状态行最前面的标题文字,比如从默认的“Working”换成更具体的动作名称。

数据流:进去的是一段新标题字符串 → 它直接替换组件内部保存的 header → 之后下一次渲染就会显示新标题。

调用关系:上层任务状态变化时会调用它。它不负责重画,只负责改数据;真正把标题画出来的是 StatusIndicatorWidget::render。

StatusIndicatorWidget::update_details114–130 ↗
fn update_details(
        &mut self,
        details: Option<String>,
        capitalization: StatusDetailsCapitalization,
        max_lines: usize,
    )

作用:更新状态行下面的详细说明文字,并决定是否把首字母大写、最多显示几行。

数据流:进去的是可选的说明文字、大小写处理方式、最大行数 → 它去掉开头空白,空字符串会被丢弃,需要时把首字母大写,并保证至少允许显示一行 → 结果保存到组件内部,等待后续换行和渲染。

调用关系:当后台任务有补充说明时,上层会调用它。之后 StatusIndicatorWidget::wrapped_details_lines 会负责把这些说明按宽度折行,StatusIndicatorWidget::render 再显示出来。

StatusIndicatorWidget::update_inline_message137–141 ↗
fn update_inline_message(&mut self, message: Option<String>)

作用:更新状态行末尾的一小段附加消息。它适合放很短的上下文,不适合放长篇说明。

数据流:进去的是可选消息 → 它去掉前后空白,空内容会被忽略 → 结果保存成 inline_message,下一次渲染时跟在运行时间和打断提示后面。

调用关系:上层如果想在同一行补一句简短信息,会调用它。StatusIndicatorWidget::render 会把它放在核心提示后面,避免遮住时间和中断快捷键。

StatusIndicatorWidget::header144–146 ↗
fn header(&self) -> &str

作用:在测试中读取当前标题,确认组件内部保存的标题是不是预期值。

数据流:进去的是组件自身 → 它返回 header 的只读文本引用 → 不修改任何数据。

调用关系:这个函数只在测试编译时存在,用来检查 update_header 或默认初始化的结果,不参与正常运行界面的绘制流程。

StatusIndicatorWidget::details149–151 ↗
fn details(&self) -> Option<&str>

作用:在测试中读取当前详细说明,确认说明文字有没有被正确保存、修剪或保留大小写。

数据流:进去的是组件自身 → 它把内部 Option<String> 转成可读的 Option<&str> 返回 → 不修改组件。

调用关系:它只给测试使用。比如 details_args_can_disable_capitalization_and_limit_lines 会用它确认 update_details 的处理结果。

StatusIndicatorWidget::set_interrupt_hint_visible153–155 ↗
fn set_interrupt_hint_visible(&mut self, visible: bool)

作用:控制状态行里要不要显示“按某键可打断”的提示。

数据流:进去的是 true 或 false → 它写入 show_interrupt_hint 字段 → 下一次渲染时,状态行会显示完整打断提示,或只显示运行时间。

调用关系:上层可以根据当前场景决定是否允许或是否提示中断。StatusIndicatorWidget::render 会读取这个开关来选择显示内容。

StatusIndicatorWidget::set_interrupt_binding157–159 ↗
fn set_interrupt_binding(&mut self, binding: Option<KeyBinding>)

作用:更换状态行显示的打断快捷键,或者清掉这个快捷键。

数据流:进去的是一个可选的 KeyBinding,也就是快捷键描述 → 它保存到 interrupt_binding → 下一次渲染会显示新的快捷键,比如从 Esc 变成 F12。

调用关系:当用户改键位或不同环境使用不同快捷键时,上层会调用它。StatusIndicatorWidget::render 会把这里保存的按键转换成可见提示。

StatusIndicatorWidget::pause_timer161–163 ↗
fn pause_timer(&mut self)

作用:暂停状态行的计时。适合在任务暂时不运行、比如等待用户操作时调用。

数据流:进去的是当前组件 → 它取当前时间,然后交给 pause_timer_at → 结果是已运行时间被固定下来,之后时间不会继续增加,直到恢复。

调用关系:这是给正常运行代码用的便捷入口。它把“现在是什么时间”的细节包起来,真正的计算由 StatusIndicatorWidget::pause_timer_at 完成。

调用图:调用 1 个内部函数(pause_timer_at);外部调用 1 个(now)。

StatusIndicatorWidget::resume_timer165–167 ↗
fn resume_timer(&mut self)

作用:恢复之前暂停的计时,让状态行继续计算任务运行了多久。

数据流:进去的是当前组件 → 它取当前时间,然后交给 resume_timer_at → 结果是组件从这个时间点重新开始累计,并请求界面重画。

调用关系:这是正常代码使用的恢复入口。它把活儿转给 StatusIndicatorWidget::resume_timer_at,后者会安排下一帧刷新。

调用图:调用 1 个内部函数(resume_timer_at);外部调用 1 个(now)。

StatusIndicatorWidget::pause_timer_at169–175 ↗
fn pause_timer_at(&mut self, now: Instant)

作用:在指定时间点暂停计时。这个版本方便测试,因为测试可以传入固定时间。

数据流:进去的是一个时间点 now → 如果已经暂停,就什么也不做;如果还在运行,就把从 last_resume_at 到 now 的时长加进 elapsed_running,并标记为暂停 → 出来后计时停住。

调用关系:StatusIndicatorWidget::pause_timer 会调用它。测试 timer_pauses_when_requested 也依赖它的行为来确认暂停期间秒数不会继续增长。

调用图:被 1 处调用(pause_timer);外部调用 1 个(saturating_duration_since)。

StatusIndicatorWidget::resume_timer_at177–184 ↗
fn resume_timer_at(&mut self, now: Instant)

作用:在指定时间点恢复计时。这个版本同样方便测试和精确控制。

数据流:进去的是一个时间点 now → 如果本来没暂停,就什么也不做;如果暂停了,就把 last_resume_at 设为 now,清掉暂停标记,并请求界面重画 → 后续 elapsed 会从 now 起继续增加。

调用关系:StatusIndicatorWidget::resume_timer 会调用它。它还会调用 FrameRequester::schedule_frame,让界面尽快刷新显示恢复后的状态。

调用图:调用 1 个内部函数(schedule_frame);被 1 处调用(resume_timer)。

StatusIndicatorWidget::elapsed_duration_at186–192 ↗
fn elapsed_duration_at(&self, now: Instant) -> Duration

作用:计算到某个时间点为止,任务实际运行了多久。暂停期间不会被算进去。

数据流:进去的是一个时间点 now → 它先拿已经累计的 elapsed_running;如果当前没暂停,再加上从 last_resume_at 到 now 的时间 → 出来的是 Duration,也就是一段时间长度。

调用关系:这是计时逻辑的核心。StatusIndicatorWidget::elapsed_seconds_at 用它拿秒数,StatusIndicatorWidget::render 用它显示运行时间。

调用图:被 2 处调用(elapsed_seconds_at, render);外部调用 1 个(saturating_duration_since)。

StatusIndicatorWidget::elapsed_seconds_at194–196 ↗
fn elapsed_seconds_at(&self, now: Instant) -> u64

作用:计算到指定时间点为止的运行秒数。它把更精细的时间长度简化成整数秒。

数据流:进去的是一个时间点 now → 它调用 elapsed_duration_at 得到总时长 → 再取其中的秒数返回,不修改组件。

调用关系:StatusIndicatorWidget::elapsed_seconds 会调用它。测试也用它检查暂停和恢复后的计时是否正确。

调用图:调用 1 个内部函数(elapsed_duration_at);被 1 处调用(elapsed_seconds)。

StatusIndicatorWidget::elapsed_seconds198–200 ↗
fn elapsed_seconds(&self) -> u64

作用:获取截至当前真实时间的运行秒数,给外部代码一个简单的读数。

数据流:进去的是组件自身 → 它读取当前时间,再调用 elapsed_seconds_at → 出来的是当前累计运行秒数。

调用关系:这是外部读取计时结果的便捷方法。它隐藏了 Instant::now 和暂停计算这些细节。

调用图:调用 1 个内部函数(elapsed_seconds_at);外部调用 1 个(now)。

StatusIndicatorWidget::wrapped_details_lines203–232 ↗
fn wrapped_details_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把详细说明按终端宽度折成多行,并在超过最大行数时用省略号收尾。

数据流:进去的是可用宽度 → 如果没有说明或宽度为 0,就返回空列表;否则给第一行加“└”前缀,后续行对齐缩进,再调用 word_wrap_lines 换行;如果行数太多,就截到上限并在最后加“…” → 出来的是可直接绘制的 Line 列表。

调用关系:StatusIndicatorWidget::desired_height 用它计算组件需要多高,StatusIndicatorWidget::render 用它真正画出说明。它把复杂的换行和省略号规则集中在一个地方。

调用图:调用 2 个内部函数(new, word_wrap_lines);被 2 处调用(desired_height, render);外部调用 6 个(from, from, width, new, format!, from)。

StatusIndicatorWidget::desired_height236–238 ↗
fn desired_height(&self, width: u16) -> u16

作用:告诉布局系统这个状态组件想占几行高度。

数据流:进去的是终端宽度 → 它调用 wrapped_details_lines 看详细说明会占几行,再加上顶部状态行的一行 → 出来的是需要的高度数字。

调用关系:布局阶段会用它提前安排空间。它不画东西,只回答“我大概要多高”,真正绘制由 StatusIndicatorWidget::render 完成。

调用图:调用 1 个内部函数(wrapped_details_lines);外部调用 1 个(try_from)。

StatusIndicatorWidget::render240–299 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把整个状态提示真正画到终端缓冲区里。包括动画、标题、耗时、打断提示、附加消息和下面的详情行。

数据流:进去的是绘制区域 area 和屏幕缓冲区 buf → 如果区域为空就退出;如果开启动画,就预约很快再画一帧;然后计算耗时、生成动画符号和标题效果、拼出快捷键提示,按宽度截断第一行,再按高度加入详情行 → 最后把这些文字写进 buf。

调用关系:这是组件在界面刷新时的主入口。它调用 fmt_elapsed_compact、activity_indicator、shimmer_text、wrapped_details_lines、truncate_line_with_ellipsis_if_overflow 等工具,把内部状态变成用户看到的画面。

调用图:调用 8 个内部函数(truncate_line_with_ellipsis_if_overflow, from_animations_enabled, activity_indicator, shimmer_text, elapsed_duration_at, wrapped_details_lines, fmt_elapsed_compact, schedule_frame_in);外部调用 11 个(from_millis, now, from, new, is_empty, from, new, with_capacity, format!, from (+1 more))。

tests::fmt_elapsed_compact_formats_seconds_minutes_hours316–327 ↗
fn fmt_elapsed_compact_formats_seconds_minutes_hours()

作用:测试时间格式化是否符合预期,覆盖秒、分钟和小时几种情况。

数据流:进去的是一组固定秒数 → 测试分别调用 fmt_elapsed_compact → 用断言确认输出字符串正好是预期格式。

调用关系:它保护 fmt_elapsed_compact 不被后续修改弄坏。因为状态栏时间非常显眼,格式一错用户马上会看到。

调用图:外部调用 1 个(assert_eq!)。

tests::renders_with_working_header330–345 ↗
fn renders_with_working_header()

作用:测试默认状态组件能正常画出“Working”状态行。

数据流:进去的是一个测试用事件通道和测试终端 → 测试创建组件并画到 80 列、2 行的假终端里 → 用快照确认画面没有意外变化。

调用关系:它通过 StatusIndicatorWidget::new 创建组件,再调用 render。快照测试像给界面拍照片,用来发现显示变化。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 3 个(new, assert_snapshot!, new)。

tests::renders_truncated348–363 ↗
fn renders_truncated()

作用:测试终端很窄时,状态行会被安全截断,而不是把界面撑乱。

数据流:进去的是 20 列宽的测试终端 → 创建默认组件并渲染 → 用快照检查截断后的画面。

调用关系:它覆盖 StatusIndicatorWidget::render 中调用 truncate_line_with_ellipsis_if_overflow 的场景,确保窄窗口下仍可读。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 3 个(new, assert_snapshot!, new)。

tests::renders_wrapped_details_panama_two_lines366–392 ↗
fn renders_wrapped_details_panama_two_lines()

作用:测试详细说明在宽度不够时会自然换成两行,而不是直接丢失。

数据流:进去的是一段固定详情文字和 30 列宽的测试终端 → 测试更新详情、隐藏打断提示、冻结计时后渲染 → 用快照确认详情刚好折成预期的两行。

调用关系:它主要验证 update_details、wrapped_details_lines 和 render 的配合,保证详情显示稳定。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 3 个(new, assert_snapshot!, new)。

tests::renders_without_spinner_when_animations_disabled395–416 ↗
fn renders_without_spinner_when_animations_disabled()

作用:测试关闭动画时,状态栏不会显示旋转图标,只显示静态文字。

数据流:进去的是 animations_enabled 为 false 的组件和测试终端 → 测试冻结时间后渲染,再读出第一行文字 → 用断言确认它以“Working (0s • esc to interrupt)”开头。

调用关系:它验证 StatusIndicatorWidget::render 和 MotionMode 的配合,确保用户关闭动画后界面真的减少动态效果。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 3 个(new, assert!, new)。

tests::renders_remapped_interrupt_hint419–436 ↗
fn renders_remapped_interrupt_hint()

作用:测试打断快捷键被改成别的键后,界面提示也会跟着改。

数据流:进去的是一个新组件和 F12 快捷键 → 测试调用 set_interrupt_binding,冻结时间后渲染 → 用快照确认提示里显示的是新的按键。

调用关系:它保护 set_interrupt_binding 到 render 这条链路,确保改键位不会只改内部数据却不改显示。

调用图:调用 4 个内部函数(new, plain, new, test_dummy);外部调用 4 个(F, new, assert_snapshot!, new)。

tests::timer_pauses_when_requested439–461 ↗
fn timer_pauses_when_requested()

作用:测试计时器暂停和恢复的规则是否正确,特别是暂停期间不应继续涨秒。

数据流:进去的是一组人为构造的时间点 → 测试先算 5 秒,再暂停到第 5 秒,检查到第 10 秒仍是 5 秒,恢复后到第 13 秒变成 8 秒 → 用断言确认结果。

调用关系:它直接检查 pause_timer_at、resume_timer_at 和 elapsed_seconds_at 的配合,是计时逻辑的安全网。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 3 个(from_secs, now, assert_eq!)。

tests::details_overflow_adds_ellipsis464–485 ↗
fn details_overflow_adds_ellipsis()

作用:测试详情文字超过允许行数时,最后一行会出现省略号。

数据流:进去的是一段会溢出的详情和很窄的宽度 → 测试更新详情后调用 wrapped_details_lines → 检查行数等于上限,并且最后内容以“…”结尾。

调用关系:它专门保护 wrapped_details_lines 的截断规则,避免长详情把底部区域撑得太高。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 2 个(assert!, assert_eq!)。

tests::details_args_can_disable_capitalization_and_limit_lines488–516 ↗
fn details_args_can_disable_capitalization_and_limit_lines()

作用:测试调用者可以选择保留详情文字原样,并把详情限制成一行。

数据流:进去的是一段命令文本、Preserve 选项和最大一行的设置 → 测试确认保存的详情没有被改大小写,再按窄宽度换行 → 检查结果只有一行且带省略号。

调用关系:它验证 update_details 的参数会真正影响 wrapped_details_lines,保证调用者能控制详情展示方式。

调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 2 个(assert!, assert_eq!)。

tui/src/status/card.rs源码 ↗
domain_logic用户运行 /status 时生成和渲染;速率限制刷新完成时也会被更新

用户输入 /status 时,程序需要把当前会话的关键信息讲清楚:正在用哪个模型,在哪个目录工作,有多大权限,账号是什么,token 花了多少,速率限制还剩多少。这个文件就像一个“排版员兼翻译员”:先从配置和运行状态里拿原始数据,再把技术味很重的内容改成人能看懂的短标签,最后按终端宽度排成带边框的卡片。它还会处理一些细节,比如窄屏时缩短进度条、隐藏默认 OpenAI 提供商、把使用量网址做成可点击链接。速率限制可能稍后刷新,所以这里用读写锁(一把允许安全读写共享数据的锁)保存状态,让卡片已经显示后也能更新限制信息。

函数细节21
StatusHistoryHandle::finish_rate_limit_refresh85–102 ↗
fn finish_rate_limit_refresh(
        &self,
        rate_limits: &[RateLimitSnapshotDisplay],
        now: DateTime<Local>,
    )

作用:速率限制数据刷新完以后,用这个函数把新数据塞回状态卡片里。这样用户再次看状态时,就不会一直看到“正在刷新”或旧数据。

数据流:进去的是一组新的速率限制快照和当前时间 → 它根据快照数量整理成统一的展示数据,再拿到写锁安全修改共享状态 → 出来没有返回值,但卡片内部的速率限制内容被换成新数据,并把“正在刷新”标记关掉。

调用关系:它是状态卡片的外部更新入口。创建卡片时会同时返回一个 StatusHistoryHandle,后台刷新完成后通过这个 handle 调用它;它把整理工作交给 compose_rate_limit_data 或 compose_rate_limit_data_many。

调用图:调用 2 个内部函数(compose_rate_limit_data, compose_rate_limit_data_many);外部调用 2 个(first, len)。

new_status_output126–158 ↗
fn new_status_output(
    config: &Config,
    account_display: Option<&StatusAccountDisplay>,
    token_info: Option<&TokenUsageInfo>,
    total_usage: &TokenUsage,
    session_id: &Option<ThreadId>,

作用:这是测试里用的简化入口,用来快速造出一张 /status 输出卡片。它只支持传一个速率限制快照,方便测试常见情况。

数据流:进去的是配置、账号、token、会话、模型名、时间等信息 → 它把单个速率限制快照包装成列表,并补上一些默认参数 → 出来是一张 CompositeHistoryCell,也就是可显示在历史记录里的组合卡片。

调用关系:它只在测试编译时存在。它不自己做卡片,而是把活交给 new_status_output_with_rate_limits。

调用图:调用 1 个内部函数(new_status_output_with_rate_limits)。

new_status_output_with_rate_limits162–198 ↗
fn new_status_output_with_rate_limits(
    config: &Config,
    account_display: Option<&StatusAccountDisplay>,
    token_info: Option<&TokenUsageInfo>,
    total_usage: &TokenUsage,
    session_id: &

作用:这是测试用的稍完整入口,可以传入多个速率限制快照。测试需要模拟不同限制状态时会用它。

数据流:进去的是配置、账号、token、会话、模型名、多个限制快照和刷新标记 → 它补上运行时提供商地址、远程连接等默认空值 → 出来是一张状态卡片,不暴露可更新的 handle。

调用关系:它被 new_status_output 调用,也只在测试里使用。真正创建卡片的工作继续交给 new_status_output_with_rate_limits_handle。

调用图:调用 1 个内部函数(new_status_output_with_rate_limits_handle);被 1 处调用(new_status_output)。

new_status_output_with_rate_limits_handle201–245 ↗
fn new_status_output_with_rate_limits_handle(
    config: &Config,
    runtime_model_provider_base_url: Option<&str>,
    remote_connection: Option<&RemoteConnectionStatus>,
    account_display: Optio

作用:这是正式创建 /status 历史输出的入口。它不仅生成状态卡片,还返回一个 handle,方便之后更新速率限制。

数据流:进去的是当前配置、运行时连接信息、账号信息、token 用量、会话信息、限制快照等 → 它先做一个显示“/status”的命令行,再创建真正的状态卡片 → 出来是组合后的历史单元和一个可刷新速率限制的 handle。

调用关系:它被测试入口间接调用,也可被真实 /status 流程使用。它把核心数据整理交给 StatusHistoryCell::new,然后把命令行和状态卡拼成 CompositeHistoryCell。

调用图:调用 3 个内部函数(new, new, new);被 1 处调用(new_status_output_with_rate_limits);外部调用 1 个(vec!)。

StatusHistoryCell::new249–374 ↗
fn new(
        config: &Config,
        runtime_model_provider_base_url: Option<&str>,
        remote_connection: Option<&RemoteConnectionStatus>,
        account_display: Option<&StatusAccountDispla

作用:这个函数把原始配置和运行状态变成状态卡片内部保存的“干净数据”。它是整张卡片的装配车间。

数据流:进去的是配置、账号、模型、权限、目录、会话、token、速率限制、远程连接等原始信息 → 它计算权限说明、模型显示名、账号显示、上下文窗口剩余量、总 token、速率限制展示数据,并建立共享锁保存可变状态 → 出来是一个 StatusHistoryCell 和一个 StatusHistoryHandle。

调用关系:它由 new_status_output_with_rate_limits_handle 调用。它会调用 format_model_provider、status_approval_label、status_permission_summary、status_permissions_label、workspace_root_suffix 等小工具,把难懂的配置值翻译成最终展示文字。

调用图:调用 12 个内部函数(from, blended_total, non_cached_input, format_model_provider, status_approval_label, status_permission_summary, status_permissions_label, workspace_root_suffix, compose_account_display, compose_model_display (+2 more));被 1 处调用(new_status_output_with_rate_limits_handle);外部调用 7 个(new, new, effective_workspace_roots, default, first, len, vec!)。

StatusHistoryCell::token_usage_spans376–392 ↗
fn token_usage_spans(&self) -> Vec<Span<'static>>

作用:把 token 用量排成一行可上色的文本。token 可以理解成模型读写文字时消耗的小单位。

数据流:进去的是卡片里已经保存好的总 token、输入 token、输出 token → 它把数字压缩成短格式,比如更适合终端显示的形式,并组合成若干 Span(带样式的小文本片段)→ 出来是一组用于渲染的文本片段。

调用关系:它只负责 token 用量这一行的内容,被 display_lines 在真正排版卡片时调用。

调用图:调用 1 个内部函数(format_tokens_compact);被 1 处调用(display_lines);外部调用 1 个(vec!)。

StatusHistoryCell::context_window_spans394–408 ↗
fn context_window_spans(&self) -> Option<Vec<Span<'static>>>

作用:把上下文窗口信息排成一行。上下文窗口就是模型一次能记住、能处理的文字容量上限。

数据流:进去的是卡片里可选的上下文窗口数据 → 如果没有这项数据,就返回空 → 如果有,就格式化剩余百分比、已用 token、窗口总量,出来是一组可显示的文本片段。

调用关系:它被 display_lines 调用。display_lines 会先判断是否有上下文窗口,再把这组文本放进状态卡片。

调用图:调用 1 个内部函数(format_tokens_compact);被 1 处调用(display_lines);外部调用 1 个(vec!)。

StatusHistoryCell::rate_limit_lines410–459 ↗
fn rate_limit_lines(
        &self,
        state: &StatusRateLimitState,
        available_inner_width: usize,
        formatter: &FieldFormatter,
    ) -> Vec<Line<'static>>

作用:把速率限制状态变成一行或多行说明。速率限制就是服务对请求次数、用量或额度的限制。

数据流:进去的是当前速率限制状态、可用宽度和字段排版器 → 它根据状态是可用、过期、不可用还是缺失,选择显示限制数据、警告语或“暂无数据” → 出来是一组终端行。

调用关系:它被 display_lines 调用来生成卡片底部的限制信息。遇到真正有行数据时,它把逐行排版交给 rate_limit_row_lines。

调用图:调用 2 个内部函数(rate_limit_row_lines, line);被 1 处调用(display_lines);外部调用 1 个(vec!)。

StatusHistoryCell::rate_limit_row_lines461–549 ↗
fn rate_limit_row_lines(
        &self,
        rows: &[StatusRateLimitRow],
        available_inner_width: usize,
        formatter: &FieldFormatter,
    ) -> Vec<Line<'static>>

作用:把每一条具体的速率限制排成好读的行。它会尽量显示进度条、剩余百分比、重置时间和补充说明。

数据流:进去的是若干限制行、终端宽度和字段排版器 → 它逐条判断是窗口型限制还是普通文字;窗口型会算剩余百分比、画进度条,窄屏时会只保留关键百分比,重置时间太长就换行 → 出来是一组已经适配宽度的显示行。

调用关系:它由 rate_limit_lines 调用,是速率限制展示的细排版部分。它会使用 format_status_limit_summary、render_status_limit_progress_bar、line_display_width 等工具,让内容既准确又不挤出屏幕。

调用图:调用 4 个内部函数(full_spans, value_width, line_display_width, format_status_limit_summary);被 1 处调用(rate_limit_lines);外部调用 8 个(from, from, with_capacity, format!, new, wrap, len, vec!)。

StatusHistoryCell::collect_rate_limit_labels551–576 ↗
fn collect_rate_limit_labels(
        &self,
        state: &StatusRateLimitState,
        seen: &mut BTreeSet<String>,
        labels: &mut Vec<String>,
    )

作用:提前收集速率限制会用到的字段名,比如“Limits”或“Warning”。这样整张卡片左侧标签能对齐。

数据流:进去的是当前限制状态、已经见过的标签集合、标签列表 → 它根据限制状态把需要出现的标签加入列表,并避免重复 → 出来没有返回值,但标签列表被补全。

调用关系:它被 display_lines 在真正排版前调用。display_lines 需要先知道所有标签,才能创建 FieldFormatter 来统一对齐。

调用图:调用 1 个内部函数(push_label);被 1 处调用(display_lines)。

status_permission_summary579–601 ↗
fn status_permission_summary(
    permission_profile: &PermissionProfile,
    cwd: &AbsolutePathBuf,
    workspace_roots: &[AbsolutePathBuf],
) -> String

作用:把底层权限描述改成更短、更像人话的说明。比如把复杂的只读加网络权限,改成“read-only with network access”。

数据流:进去的是权限配置、当前目录和工作区根目录列表 → 它先调用 summarize_permission_profile 得到底层摘要,再把常见长句压缩成固定短语 → 出来是用于状态卡片的权限摘要字符串。

调用关系:它被 StatusHistoryCell::new 调用,是权限显示链路的第一步。后面 status_permissions_label 会继续把它包装成最终标签。

调用图:被 1 处调用(new);外部调用 1 个(summarize_permission_profile)。

workspace_root_suffix603–617 ↗
fn workspace_root_suffix(
    workspace_roots: &[AbsolutePathBuf],
    cwd: &AbsolutePathBuf,
) -> Option<String>

作用:找出当前目录之外的额外工作区目录,并拼成显示后缀。这样用户能知道工具不只会碰当前目录。

数据流:进去的是所有工作区根目录和当前目录 → 它过滤掉当前目录,只保留额外目录,并把它们拼成类似“[目录1, 目录2]”的文本 → 如果没有额外目录就返回空。

调用关系:它被 StatusHistoryCell::new 调用。结果会传给 status_permissions_label,用来显示 Workspace 权限到底覆盖哪些额外位置。

调用图:被 1 处调用(new);外部调用 2 个(format!, iter)。

status_permissions_label619–684 ↗
fn status_permissions_label(
    active_permission_profile: Option<&ActivePermissionProfile>,
    permission_profile: &PermissionProfile,
    approval_policy: AskForApproval,
    sandbox: &str,
    ap

作用:生成最终显示在“Permissions”这一栏里的权限文字。它把权限档案、沙箱和审批规则合成一句用户能判断风险的话。

数据流:进去的是当前启用的权限档案、实际权限配置、审批策略、沙箱摘要、审批说明和工作区后缀 → 它识别内置的只读、工作区、完全访问等情况,也处理自定义权限 → 出来是一句最终标签,比如“Read Only (...)”“Workspace (...)”或“Full Access”。

调用关系:它被 StatusHistoryCell::new 调用。遇到工作区类沙箱时,它会调用 decorate_workspace_sandbox_label 把额外工作区目录补进去。

调用图:调用 1 个内部函数(decorate_workspace_sandbox_label);被 1 处调用(new);外部调用 1 个(format!)。

decorate_workspace_sandbox_label686–691 ↗
fn decorate_workspace_sandbox_label(sandbox: &str, workspace_root_suffix: Option<&str>) -> String

作用:给工作区权限说明补上额外目录后缀。它只在说明确实是 workspace 开头时才加,避免乱改别的权限文字。

数据流:进去的是沙箱说明和可选后缀 → 如果有后缀且沙箱说明是 workspace 类,就把两者拼起来 → 否则原样返回沙箱说明。

调用关系:它只被 status_permissions_label 调用,是权限标签生成里的一个小修饰工具。

调用图:被 1 处调用(status_permissions_label);外部调用 1 个(format!)。

status_approval_label693–706 ↗
fn status_approval_label(
    approval_policy: AskForApproval,
    approvals_reviewer: ApprovalsReviewer,
    approval: &str,
) -> String

作用:把审批策略翻译成更清楚的用户提示。审批就是工具做某些操作前要不要先问用户。

数据流:进去的是审批策略、谁负责审批、以及原始审批文字 → 如果策略是按需审批,它会区分“自动帮我批准”和“询问我批准” → 其他情况直接返回原始审批文字。

调用关系:它被 StatusHistoryCell::new 调用。生成的文字会进入 status_permissions_label,最后显示在权限栏里。

调用图:被 1 处调用(new)。

StatusHistoryCell::display_lines709–874 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:这是把状态卡片真正画出来的主函数。它根据终端宽度,把所有信息排成带边框、能换行、能截断的多行文本。

数据流:进去的是终端宽度和卡片内部保存的数据 → 它计算可用宽度,读取共享的速率限制和 Agents 摘要,收集字段标签,生成模型、目录、权限、账号、会话、token、上下文窗口、限制等行,并按宽度裁剪加边框 → 出来是一组可以直接显示在终端里的 Line。

调用关系:它是渲染核心,被 raw_lines 和 display_hyperlink_lines 调用。它会把不同小任务交给 collect_rate_limit_labels、token_usage_spans、context_window_spans、rate_limit_lines、format_directory_display 等函数。

调用图:调用 10 个内部函数(collect_rate_limit_labels, context_window_spans, rate_limit_lines, token_usage_spans, from_labels, push_label, format_directory_display, new, adaptive_wrap_lines, word_wrap_lines);被 2 处调用(display_hyperlink_lines, raw_lines);外部调用 8 个(from, from, new, new, with_border_with_inner_width, matches!, from, vec!)。

StatusHistoryCell::raw_lines876–878 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成不带复杂样式的纯文本版状态卡片。适合记录日志、复制文本或做测试比较。

数据流:进去的是卡片本身 → 它用极大的宽度调用 display_lines 先生成完整显示行,再用 plain_lines 去掉或简化样式 → 出来是纯文本风格的行列表。

调用关系:它是 HistoryCell 接口的一部分。它依赖 display_lines 复用同一套内容,只是在最后变成更朴素的版本。

调用图:调用 2 个内部函数(plain_lines, display_lines)。

format_model_provider913–931 ↗
fn format_model_provider(config: &Config, runtime_base_url: Option<&str>) -> Option<String>

作用:决定“模型提供商”这一栏要不要显示,以及显示什么。默认 OpenAI 通常不用显示,非默认或自定义地址才值得提醒用户。

数据流:进去的是配置和可选的运行时基础地址 → 它选择提供商名字,清理基础地址,判断是不是默认 OpenAI → 默认 OpenAI 返回空;否则返回“提供商名”或“提供商名 - 地址”。

调用关系:它被 StatusHistoryCell::new 调用。它会把地址清理工作交给 sanitize_base_url,防止把用户名、密码、查询参数之类敏感或杂乱内容显示出来。

调用图:被 1 处调用(new);外部调用 1 个(format!)。

sanitize_base_url933–947 ↗
fn sanitize_base_url(raw: &str) -> Option<String>

作用:把模型服务地址清理成适合展示的安全版本。它会去掉账号密码、查询参数和片段,避免状态卡片泄露敏感信息。

数据流:进去的是原始网址字符串 → 它先去掉前后空白,空字符串或解析失败就返回空;解析成功后清掉用户名、密码、query、fragment,并去掉末尾斜杠 → 出来是干净的网址字符串。

调用关系:它被 format_model_provider 调用。它只负责清洗地址,是否显示提供商由 format_model_provider 决定。

调用图:外部调用 1 个(parse)。

tui/src/token_usage.rs源码 ↗
domain_logiccross-cutting

大模型聊天时,每段文字都会被拆成 token(可以粗略理解成“字词小块”),模型一次能记住的 token 数量也有上限,叫上下文窗口。这个文件就是给终端界面准备一套“记账本”:TokenUsage 记录输入、缓存输入、输出、推理输出和总数;TokenUsageInfo 把本轮用量、累计用量和模型窗口大小放在一起。它还会做一些贴心修正,比如负数一律当成 0,避免界面显示奇怪数字;缓存输入会单独算,因为它不是普通新输入;剩余窗口百分比会扣掉一个 12000 token 的基准区间,只展示真正可用部分的余量。最后,它实现了显示格式,把数字加上千位分隔符,拼成用户能直接读懂的一行文字。

函数细节7
TokenUsage::is_zero21–23 ↗
fn is_zero(&self) -> bool

作用:判断这份 token 用量是不是完全没有记录。界面或上层逻辑可以用它来决定要不要显示用量信息。

数据流:进去的是一个 TokenUsage 记录 → 它只看 total_tokens 这个总数是不是 0 → 出来一个 true 或 false,不改动任何数据。

调用关系:它是一个简单检查点,适合在显示或统计前先问一句“这里有没有东西可显示”。调用图里没有显示它再去调用别人,也没有显示谁固定调用它。

TokenUsage::cached_input25–27 ↗
fn cached_input(&self) -> i64

作用:取出“缓存输入”的 token 数,并保证结果不会是负数。缓存输入可以理解成模型以前见过、这次复用的内容。

数据流:进去的是 TokenUsage 里的 cached_input_tokens → 它把这个数和 0 比较,负数就按 0 处理 → 出来一个安全的缓存输入数量,不改原始记录。

调用关系:它给 TokenUsage::non_cached_input 打基础。non_cached_input 要先知道有多少输入是缓存的,才能算出真正新输入了多少。

调用图:被 1 处调用(non_cached_input)。

TokenUsage::non_cached_input29–31 ↗
fn non_cached_input(&self) -> i64

作用:算出没有走缓存的输入 token,也就是这次真正新消耗的输入部分。

数据流:进去的是 input_tokens 和通过 TokenUsage::cached_input 得到的缓存输入数 → 它用总输入减去缓存输入,并把负数压到 0 → 出来一个非缓存输入数量。

调用关系:它会调用 TokenUsage::cached_input,避免缓存数为负导致计算乱掉。TokenUsage::blended_total 又会用它,把“真正输入”和输出合在一起算展示用的总量。

调用图:调用 1 个内部函数(cached_input);被 1 处调用(blended_total)。

TokenUsage::blended_total33–35 ↗
fn blended_total(&self) -> i64

作用:算一个更适合展示给人的总 token 数:非缓存输入加输出。这样用户看到的是更接近实际新消耗的数量。

数据流:进去的是 TokenUsage 当前记录 → 它先通过 TokenUsage::non_cached_input 算新输入,再加上 output_tokens;输出为负时按 0 兜底,最后总数也不让它低于 0 → 出来一个用于显示的合计值。

调用关系:它站在显示层前面,把底层的输入、缓存和输出整理成一个用户更容易理解的总数。TokenUsage::fmt 会间接依赖这个结果来打印用量摘要。

调用图:调用 1 个内部函数(non_cached_input)。

TokenUsage::tokens_in_context_window39–41 ↗
fn tokens_in_context_window(&self) -> i64

作用:返回原始 total_tokens。对最近一次用量来说,它表示当前上下文里塞了多少 token;对累计用量来说,它表示整个会话累计用了多少。

数据流:进去的是 TokenUsage 记录 → 它直接读取 total_tokens → 出来这个原始总数,不做修正,也不改数据。

调用关系:它被 TokenUsage::percent_of_context_window_remaining 使用。后者需要先知道当前窗口里已经用了多少 token,才能估算还剩多少空间。

调用图:被 1 处调用(percent_of_context_window_remaining)。

TokenUsage::percent_of_context_window_remaining43–53 ↗
fn percent_of_context_window_remaining(&self, context_window: i64) -> i64

作用:根据模型的上下文窗口大小,估算还剩百分之多少空间。它用于提醒用户:这段对话还能继续塞多少内容,快不快满了。

数据流:进去的是当前 TokenUsage 和 context_window 窗口大小 → 如果窗口不大于 12000,就直接返回 0;否则先扣掉 12000 的基准区间,再用 TokenUsage::tokens_in_context_window 得到已用量,算出剩余比例 → 出来一个 0 到 100 之间的整数百分比。

调用关系:它调用 TokenUsage::tokens_in_context_window 拿到当前已用 token。它通常会被界面显示逻辑拿来做“上下文还剩多少”的提示,相当于油表里的剩余油量百分比。

调用图:调用 1 个内部函数(tokens_in_context_window)。

TokenUsage::fmt64–88 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把 TokenUsage 变成一行好读的文字。比如显示总量、输入、缓存输入、输出,以及推理输出。

数据流:进去的是 TokenUsage 和一个格式化输出目标 → 它计算展示用总量、非缓存输入、缓存输入、输出和推理输出,并把数字加上分隔符;有缓存或推理输出时才额外显示括号说明 → 出来的是写入格式化目标的一段文字,原始数据不变。

调用关系:它是 TokenUsage 给终端界面看的“翻译器”。当外部代码需要把 TokenUsage 当字符串展示时,会走这个格式化实现;它最后把拼好的内容交给 Rust 的 write! 宏写出去。

调用图:外部调用 1 个(write!)。

tui/src/chatwidget/tokens/chart.rs源码 ↗
domain_logicrequest handling: 用户执行 /usage 并显示结果时活跃

这个文件专门做一件事:把后端返回的 token 使用数据,变成终端界面能显示的几行文字。token 可以理解成模型处理文字的“计量单位”。它先显示总量、峰值、连续使用天数、最长任务时长这些摘要,再按最近 52 周画出活动图。图有三种看法:每天一格、每周一根柱、累计增长柱。它会根据终端宽度决定能显示多少列,太窄就提示用户加宽窗口。它还会把日期对齐到周日开始,忽略坏日期、未来日期和负数,避免图表被脏数据弄乱。整体像一个小型报表生成器:输入是服务器数据和今天日期,输出是带颜色、带符号的终端文本行。

函数细节27
TokenActivityView::parse45–52 ↗
fn parse(value: &str) -> Option<Self>

作用:把用户在 /usage 后面输入的参数,翻译成图表模式。比如空参数或 daily 表示每日图,weekly 表示每周图,cumulative 表示累计图。

数据流:进去的是一段用户输入的文字 → 它先去掉空格、转成小写,再和支持的几个名字比较 → 出来的是对应的视图类型;如果不认识,就返回空值,让上层知道这是不支持的参数。

调用关系:它会在命令分发时被 dispatch_prepared_command_with_args 调用。也就是说,用户刚输入 /usage weekly 这类命令时,先由它判断用户到底想看哪一种图。

调用图:被 1 处调用(dispatch_prepared_command_with_args)。

TokenActivityView::label54–60 ↗
fn label(self) -> &'static str

作用:把图表模式变成人能读懂的英文标签,比如 Daily、Weekly、Cumulative。这个标签适合放在界面或日志里。

数据流:进去的是一个图表模式 → 它按模式做简单匹配 → 出来的是固定的文字标签,不改动任何外部数据。

调用关系:它是 TokenActivityView 自己的小工具。需要把内部枚举值展示给人看时,可以用它把机器里的选项换成可读文字。

loaded_lines63–86 ↗
fn loaded_lines(
    view: TokenActivityView,
    response: &GetAccountTokenUsageResponse,
    today: NaiveDate,
    width: u16,
) -> Vec<Line<'static>>

作用:生成 /usage 卡片真正要显示的所有文本行。它是这个文件的主入口:摘要、空行、图表、提示文字都从这里拼出来。

数据流:进去的是用户选择的视图、服务器返回的使用数据、今天的日期和终端宽度 → 它先做标题和摘要,再检查有没有每日历史数据;有数据就继续生成图表,没有就显示“历史不可用” → 出来是一组终端文本行,供界面直接显示。

调用关系:它被 display_lines 调用,处在“数据已经拿到,准备画出来”的阶段。它把摘要交给 summary_lines,把图表交给 chart_lines,并用 graph_width 保证摘要和图表宽度协调。

调用图:调用 3 个内部函数(chart_lines, graph_width, summary_lines);被 1 处调用(display_lines);外部调用 2 个(default, vec!)。

chart_lines88–138 ↗
fn chart_lines(
    view: TokenActivityView,
    buckets: &[codex_app_server_protocol::AccountTokenUsageDailyBucket],
    today: NaiveDate,
    width: u16,
) -> Vec<Line<'static>>

作用:把最近一年 token 使用情况画成具体图表行。它负责决定每个格子或柱子是什么颜色、什么符号,以及底部说明该怎么写。

数据流:进去的是视图类型、每日数据桶、今天日期和终端宽度 → 它先把原始日期数据整理成固定 52 周的数组,再按宽度裁掉看不下的列,计算颜色等级,最后拼出月份、星期或坐标、图形、图例和页脚 → 出来是一组图表文本行。

调用关系:它由 loaded_lines 调用,是图表绘制的核心。它会请 daily_values 清洗数据,请 levels_for_view 算强弱等级,请 month_labels 做月份标签;每日图用 legend_line,周图和累计图用 bar_caption

调用图:调用 9 个内部函数(bar_caption, cell_date, daily_values, legend_line, levels_for_view, month_labels, current, shown_columns, view_footer);被 1 处调用(loaded_lines);外部调用 4 个(default, styled, new, vec!)。

shown_columns140–146 ↗
fn shown_columns(width: u16) -> usize

作用:根据终端宽度算出图表最多能显示多少周。因为每周占一列,窗口窄时不能硬画满 52 周。

数据流:进去的是终端宽度 → 它扣掉左侧标签宽度,再按每列大约两个字符来换算 → 出来是可显示的周数,最多不超过 52。

调用关系:它被 chart_lines 用来决定实际画多少列,也被 graph_width 用来反推图表实际宽度。它相当于图表排版前的尺子。

调用图:被 2 处调用(chart_lines, graph_width);外部调用 1 个(from)。

graph_width148–153 ↗
fn graph_width(width: u16) -> u16

作用:算出图表区域实际会占多少字符宽。摘要行会用这个宽度来排版,避免摘要比图表宽得离谱或对不齐。

数据流:进去的是终端宽度 → 如果是特殊的无限宽模式就原样返回;否则先问 shown_columns 能显示多少列,再换算成字符宽度 → 出来的是图表实际宽度。

调用关系:它被 loaded_lines 调用,然后这个宽度会交给 summary_lines。这样标题数字区和下面的活动图能看起来像同一张卡片。

调用图:调用 1 个内部函数(shown_columns);被 1 处调用(loaded_lines)。

summary_lines155–173 ↗
fn summary_lines(response: &GetAccountTokenUsageResponse, width: u16) -> Vec<Line<'static>>

作用:生成图表上方的几项关键数字。比如总 token、单日峰值、连续使用天数、最长任务时长。

数据流:进去的是服务器响应和可用宽度 → 它从 summary 里取出几项数据,分别格式化成短文字,再按宽度拆成一行或多行 → 出来的是摘要文本行。

调用关系:它由 loaded_lines 调用,负责图表前的“仪表盘”。它把具体格式化工作交给 format_optional_tokensformat_streakformat_optional_duration,再用 pack_fields 安排换行。

调用图:调用 4 个内部函数(format_optional_duration, format_optional_tokens, format_streak, pack_fields);被 1 处调用(loaded_lines)。

pack_fields177–198 ↗
fn pack_fields(fields: &[(&str, String)], width: u16) -> Vec<Vec<usize>>

作用:把摘要里的几个字段尽量塞进少量行里,同时不超过宽度。它像排版时自动换行的助手。

数据流:进去的是字段列表和最大宽度 → 它按原顺序一个个尝试加入当前行;如果放进去会超宽,就开新行 → 出来的是每一行应该包含哪些字段的索引。

调用关系:它被 summary_lines 调用。它会临时用 summary_line 估算某组字段实际有多宽,然后决定是否换行。

调用图:调用 1 个内部函数(summary_line);被 1 处调用(summary_lines);外部调用 4 个(new, take, from, vec!)。

summary_line200–211 ↗
fn summary_line(fields: &[(&str, String)], indexes: &[usize]) -> Line<'static>

作用:把若干个摘要字段拼成一行。它会把字段名和数字用不同样式显示,中间用圆点隔开。

数据流:进去的是全部字段和本行要显示的字段编号 → 它按编号取出标签和值,给标签套较淡样式,给数字套醒目样式 → 出来是一条带样式的终端文本行。

调用关系:它主要服务 pack_fields 和摘要排版。pack_fields 用它判断宽度,最终摘要行也基于它生成。样式来自 label_stylenumeric_style

调用图:调用 2 个内部函数(label_style, numeric_style);被 1 处调用(pack_fields);外部调用 3 个(styled, new, format!)。

align_summary_line213–219 ↗
fn align_summary_line(mut line: Line<'static>, width: u16) -> Line<'static>

作用:给摘要行前面加一点缩进,让它和整张卡片的视觉位置更协调。特殊的原始复制模式下不加缩进。

数据流:进去的是一条摘要行和宽度 → 如果宽度是特殊的无限宽值,就原样返回;否则在最前面插入一个空格 → 出来的是调整过的摘要行。

调用关系:它在摘要行生成后使用,属于最后的排版修饰。它不改变数字含义,只改变显示位置。

format_optional_tokens221–225 ↗
fn format_optional_tokens(value: Option<i64>) -> String

作用:把可能缺失的 token 数字变成短文字。缺失时显示 -,有值时用紧凑格式显示。

数据流:进去的是一个可能存在、也可能不存在的 token 数 → 有数字就交给 format_tokens_compact 缩短显示,比如大数不写得太长;没有数字就输出 - → 出来的是一段可放进摘要里的文字。

调用关系:它被 summary_lines 调用,用在 Lifetime 和 Peak 这类 token 数字段上。它让上层不用反复处理“数据为空怎么办”。

调用图:被 1 处调用(summary_lines)。

format_streak229–237 ↗
fn format_streak(current: Option<i64>, longest: Option<i64>) -> String

作用:把当前连续使用天数和历史最长连续天数合成一个字段。这样用户一眼能看到现在连续多久,以及最好成绩是多少。

数据流:进去的是当前连续天数和最长连续天数,二者都可能缺失 → 如果两者相同就只显示一个天数;如果不同就显示当前值并附上 best;缺失的地方用 - 表示 → 出来的是摘要里的 streak 文本。

调用关系:它被 summary_lines 调用,专门处理 Streak 字段。它把两个相关数字合并,避免界面上多占一个字段。

调用图:被 1 处调用(summary_lines);外部调用 1 个(format!)。

format_optional_duration239–254 ↗
fn format_optional_duration(value: Option<i64>) -> String

作用:把可能缺失的秒数变成容易读的时长。比如 90 秒显示成 1m,7200 秒显示成 2h。

数据流:进去的是一个可能存在的秒数 → 没有就输出 -;有的话先把负数当成 0,再换算成小时和分钟,必要时保留秒 → 出来的是短时长文字。

调用关系:它被 summary_lines 调用,用来显示 Longest task。它把机器喜欢的秒数,换成人更容易理解的时间。

调用图:被 1 处调用(summary_lines)。

numeric_style256–259 ↗
fn numeric_style() -> Style

作用:决定数字该用什么颜色和样式显示。数字通常要比标签更醒目,让用户先看到重点。

数据流:进去不需要业务数据 → 它先尝试按当前主题查找适合数字的前景色;如果主题没给,就默认用绿色 → 出来的是一个文本样式。

调用关系:它被 summary_lineview_footer 使用。前者用它突出摘要数字,后者用它突出当前选中的图表模式。

调用图:调用 1 个内部函数(foreground_style_for_scopes);被 2 处调用(summary_line, view_footer)。

label_style261–263 ↗
fn label_style() -> Style

作用:决定说明文字、标签、辅助提示该怎么显示。它通常更淡,避免抢过数字和图形的注意力。

数据流:进去不需要业务数据 → 它尝试从当前主题里取“注释”风格;取不到就用默认的变暗样式 → 出来的是一个文本样式。

调用关系:它被很多显示函数共用,比如 summary_lineweekday_labellegend_linebar_captionview_footer。它保证这些辅助文字风格统一。

调用图:调用 1 个内部函数(foreground_style_for_scopes);被 5 处调用(bar_caption, legend_line, summary_line, view_footer, weekday_label)。

weekday_label265–291 ↗
fn weekday_label(view: TokenActivityView, row: usize) -> Span<'static>

作用:生成图表左侧的小标签。每日图显示星期几,柱状图则显示粗略的纵轴,比如 max 和 0。

数据流:进去的是当前视图和第几行 → 如果是每日图,就按行号输出 Su、Mo 等星期标签;如果是周图或累计图,就在顶部标 max、底部标 0 → 出来的是带淡色样式的左侧标签。

调用关系:它在 chart_lines 画每一行时使用。它让同一个 7 行高的图表,在不同视图下有不同但合理的读法。

调用图:调用 1 个内部函数(label_style);外部调用 1 个(styled)。

legend_line293–306 ↗
fn legend_line(palette: &TokenActivityPalette) -> Line<'static>

作用:生成每日图下面的颜色图例。它告诉用户浅色代表少,深色代表多。

数据流:进去的是当前配色方案 → 它从低到高生成 5 个活动等级的小符号,并在两边加上 Less 和 More → 出来的是一条图例行。

调用关系:它被 chart_lines 在每日视图下调用。因为每日图每格代表一天,需要这个图例帮助用户理解颜色深浅。

调用图:调用 3 个内部函数(label_style, for_level, glyph);被 1 处调用(chart_lines);外部调用 2 个(styled, vec!)。

bar_caption310–328 ↗
fn bar_caption(view: TokenActivityView, values: &[i64]) -> Line<'static>

作用:给每周图和累计图生成说明文字。它解释每根柱子代表什么,以及最高柱对应多少 token。

数据流:进去的是视图类型和每日值数组 → 它先汇总成每周总数;每周图取最大周用量,累计图取总用量;如果没有活动就显示无活动提示 → 出来的是一条说明行。

调用关系:它被 chart_lines 在 Weekly 和 Cumulative 两种视图下调用。它替代每日图的颜色图例,因为柱状图的含义不一样。

调用图:调用 2 个内部函数(label_style, weekly_totals);被 1 处调用(chart_lines);外部调用 2 个(styled, vec!)。

month_labels353–377 ↗
fn month_labels(today: NaiveDate, first_column: usize, shown_columns: usize) -> Line<'static>

作用:生成图表顶部的月份标签,比如 Jan、Feb。它帮助用户把横向的周列和真实月份对应起来。

数据流:进去的是今天日期、第一列位置和显示列数 → 它从图表起始日期开始按周检查,如果某周落在月初附近,就尝试把月份缩写放到对应位置;放不下或会重叠就跳过 → 出来的是一条月份标签行。

调用关系:它被 chart_lines 在绘图前调用。它依赖 chart_start 找到整张图最左边对应哪一天。

调用图:调用 1 个内部函数(chart_start);被 1 处调用(chart_lines);外部调用 2 个(days, vec!)。

daily_values384–408 ↗
fn daily_values(
    buckets: &[codex_app_server_protocol::AccountTokenUsageDailyBucket],
    today: NaiveDate,
) -> Vec<i64>

作用:把服务器返回的每日记录整理成固定长度的数组。图表需要 52 周乘 7 天,也就是 364 个格子,它负责把数据放进正确格子。

数据流:进去的是每日数据桶和今天日期 → 它算出图表起点和终点,逐条解析日期;坏日期、范围外日期、未来日期会被忽略,同一天重复记录会相加,负 token 不会抵消已有活动 → 出来的是按图表格子顺序排列的 364 个数。

调用关系:它被 chart_lines 调用,是原始数据进入图表前的清洗关口。它用 chart_start 保证所有视图都从同一个周日对齐。

调用图:调用 1 个内部函数(chart_start);被 1 处调用(chart_lines);外部调用 3 个(new, days, parse_from_str)。

levels_for_view410–425 ↗
fn levels_for_view(values: &[i64], view: TokenActivityView) -> Vec<usize>

作用:根据当前视图,把真实 token 数换成绘图等级。等级决定每个格子或柱子该显示多强。

数据流:进去的是每日值数组和视图类型 → 每日图走 graded_levels,按每天相对最大值分 0 到 4 级;每周图先用 weekly_totals 汇总再走 bar_levels;累计图先做周累计再走 bar_levels → 出来的是和图表格子对应的等级数组。

调用关系:它被 chart_lines 调用,处在“数据已经清洗好,准备变成颜色和符号”的阶段。它把不同视图的尺度差异封装起来。

调用图:调用 3 个内部函数(bar_levels, graded_levels, weekly_totals);被 1 处调用(chart_lines)。

graded_levels427–439 ↗
fn graded_levels(values: &[i64]) -> Vec<usize>

作用:给每日图计算每一天的强弱等级。它不是直接显示数字,而是把一天的用量分成几个档位。

数据流:进去的是每日 token 数数组 → 它找到最大值,再把每一天按相对比例分成 0、1、2、3、4 级;0 或全都为 0 时就是最低级 → 出来的是每日活动等级数组。

调用关系:它由 levels_for_view 在 Daily 视图下调用。结果会被 chart_lines 用来选择每日格子的颜色和符号。

调用图:被 1 处调用(levels_for_view)。

weekly_totals441–446 ↗
fn weekly_totals(values: &[i64]) -> Vec<i64>

作用:把每天的数据按 7 天一组加成每周总数。周图和累计图都需要先知道每周用了多少。

数据流:进去的是按天排列的数值数组 → 它每 7 个数切一组并求和 → 出来的是每周 token 总数列表。

调用关系:它被 levels_for_view 用来准备周图和累计图,也被 bar_caption 用来算说明文字里的最高值或总值。

调用图:被 2 处调用(bar_caption, levels_for_view)。

bar_levels448–461 ↗
fn bar_levels(totals: &[i64]) -> Vec<usize>

作用:把每周总数转换成 7 行高的柱状图等级。用量越接近最大值,柱子越高。

数据流:进去的是每周总数列表 → 它找出最大周值,再把每周数值按比例换算成 0 到 7 的柱高;柱子覆盖到的行给高等级,没覆盖到的行给 0 → 出来的是按图表格子顺序排列的等级数组。

调用关系:它由 levels_for_view 在 Weekly 和 Cumulative 视图下调用。chart_lines 后面会根据这些等级画出柱子的有无和高度。

调用图:被 1 处调用(levels_for_view)。

chart_start463–466 ↗
fn chart_start(today: NaiveDate) -> NaiveDate

作用:算出这张 52 周图最左上角对应哪一天。它总是把起点对齐到周日,方便按星期排成整齐日历。

数据流:进去的是今天日期 → 它先退回到本周周日,再往前退 51 周 → 出来的是图表最早那一周的周日日期。

调用关系:它被 daily_valuesmonth_labelscell_date 使用。所有和日期位置有关的函数都靠它保持同一套坐标。

调用图:被 3 处调用(cell_date, daily_values, month_labels);外部调用 4 个(days, weeks, weekday, from)。

cell_date468–470 ↗
fn cell_date(today: NaiveDate, index: usize) -> Option<NaiveDate>

作用:把图表里的某个格子编号换成真实日期。这样程序能判断这个格子是不是未来日期。

数据流:进去的是今天日期和格子编号 → 它先用 chart_start 找到起点,再加上编号对应的天数 → 出来的是这个格子的日期;如果日期计算越界,就返回空值。

调用关系:它被 chart_lines 使用,主要用于每日图。遇到今天之后的格子时,图表会留空,不会假装未来也有数据。

调用图:调用 1 个内部函数(chart_start);被 1 处调用(chart_lines);外部调用 1 个(days)。

外部事件适配器与后端数据源

这些文件将后端进程、速率限制、exec 和 pull-stream 事件转换为数据,供 TUI 随后投射到转录和状态 UI 中。

codex-api/src/rate_limits.rs源码 ↗
io_transportrequest handling / response streaming

服务器会用两种方式告诉客户端限额情况:一种是 HTTP 响应头,也就是每次请求附带的小纸条;另一种是 WebSocket 事件,也就是长连接里推来的消息。这个文件专门把这些零散信息翻译成 RateLimitSnapshot,像把不同格式的账单整理成同一张表。它会识别默认的 codex 限额,也会扫描其它命名限额;会读取主要窗口、次要窗口、点数余额、促销提示和“触顶原因”。它还很小心:空字符串会丢掉,非法数字会忽略,限额名字会统一成小写并用下划线,避免同一个限额因为写法不同被当成两份。测试部分则确保常见响应头都能被正确读出来。

函数细节22
RateLimitError::fmt17–19 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把 RateLimitError 里的错误消息变成可以显示给人的文字。这样日志、报错界面或调试输出看到的不是结构体,而是一句清楚的错误说明。

数据流:进去的是一个格式化器和这个错误对象 → 它取出对象里的 message 字段 → 输出同样的文字给格式化系统,不改动原对象。

调用关系:它是 RateLimitError 的显示方式。外部代码只要把这个错误当作可打印内容使用,就会走到这里;它最后把实际写字的工作交给标准的 write! 宏。

调用图:外部调用 1 个(write!)。

parse_default_rate_limit23–25 ↗
fn parse_default_rate_limit(headers: &HeaderMap) -> Option<RateLimitSnapshot>

作用:读取默认的 Codex 限额响应头。别人不想关心具体限额名字时,就用这个函数拿最常见的 codex 限额信息。

数据流:进去的是一组 HTTP 响应头 → 它把限额名字留空,表示使用默认 codex → 返回一个 RateLimitSnapshot,里面可能有使用比例、窗口时间、恢复时间和点数信息。

调用关系:它是 parse_rate_limit_for_limit 的简化入口。parse_all_rate_limits 会先调用它,保证默认 codex 限额一定被放进结果里。

调用图:调用 1 个内部函数(parse_rate_limit_for_limit);被 1 处调用(parse_all_rate_limits)。

parse_all_rate_limits28–51 ↗
fn parse_all_rate_limits(headers: &HeaderMap) -> Vec<RateLimitSnapshot>

作用:一次性扫描所有已知的限额响应头,而不只看默认 codex。服务器如果同时返回多个模型或功能的限额,这个函数会把它们都整理出来。

数据流:进去的是整包 HTTP 响应头 → 它先解析默认 codex,再遍历所有响应头名字,找出像 x-某限额-primary-used-percent 这样的限额族 → 对每个找到的限额生成快照,最后输出一组 RateLimitSnapshot。

调用关系:响应流处理代码 spawn_response_stream 会用它来更新客户端看到的限额状态。它内部先找 parse_default_rate_limit,再用 header_name_to_limit_id 从响应头名里提取限额编号,最后为每个编号调用解析逻辑。测试也覆盖了它能读多组限额、以及空响应头仍返回默认快照。

调用图:调用 2 个内部函数(header_name_to_limit_id, parse_default_rate_limit);被 3 处调用(parse_all_rate_limits_includes_default_codex_snapshot, parse_all_rate_limits_reads_all_limit_families, spawn_response_stream);外部调用 3 个(new, keys, new)。

parse_rate_limit_for_limit57–100 ↗
fn parse_rate_limit_for_limit(
    headers: &HeaderMap,
    limit_id: Option<&str>,
) -> Option<RateLimitSnapshot>

作用:按指定限额编号读取一整套响应头。比如默认 codex,或者 codex_secondary、codex_bengalfox 这类服务器给出的细分限额。

数据流:进去的是 HTTP 响应头和一个可选的限额编号 → 它把编号统一成响应头使用的写法,拼出 primary、secondary、limit-name 等响应头名字 → 分别读取窗口信息、点数余额和显示名称 → 输出一个 RateLimitSnapshot。

调用关系:这是响应头解析的核心函数。parse_default_rate_limit 会把默认情况交给它;测试直接调用它检查默认限额、次级限额和 limit-name 响应头是否都能正确转换。它会把窗口读取交给 parse_rate_limit_window,把点数读取交给 parse_credits_snapshot,把名字统一交给 normalize_limit_id。

调用图:调用 4 个内部函数(normalize_limit_id, parse_credits_snapshot, parse_header_str, parse_rate_limit_window);被 4 处调用(parse_default_rate_limit, parse_rate_limit_for_limit_defaults_to_codex_headers, parse_rate_limit_for_limit_prefers_limit_name_header, parse_rate_limit_for_limit_reads_secondary_headers);外部调用 1 个(format!)。

parse_rate_limit_event133–165 ↗
fn parse_rate_limit_event(payload: &str) -> Option<RateLimitSnapshot>

作用:解析 WebSocket 推来的限额事件。WebSocket 可以理解成长开的消息通道,服务器不等下一次请求,也能主动通知客户端限额变化。

数据流:进去的是一段 JSON 字符串 → 它先把字符串解析成事件结构,再确认事件类型必须是 codex.rate_limits → 取出主窗口、次窗口、点数、套餐类型和限额名 → 输出统一的 RateLimitSnapshot;如果不是目标事件或 JSON 无法解析,就返回空。

调用关系:run_websocket_response_stream 在处理实时响应流时会调用它。它把 JSON 解析交给 serde_json::from_str,把窗口字段转换交给 map_event_window。

调用图:调用 1 个内部函数(map_event_window);被 1 处调用(run_websocket_response_stream);外部调用 1 个(from_str)。

map_event_window167–174 ↗
fn map_event_window(window: Option<&RateLimitEventWindow>) -> Option<RateLimitWindow>

作用:把 WebSocket 事件里的一个限额窗口,转换成项目通用的 RateLimitWindow。它是一个小翻译器,专门处理字段名字和结构不同的问题。

数据流:进去的是可选的事件窗口 → 如果没有窗口,就输出空 → 如果有,就复制使用百分比、窗口分钟数和恢复时间,生成 RateLimitWindow。

调用关系:它只服务于 parse_rate_limit_event。parse_rate_limit_event 分别拿 primary 和 secondary 调它,把事件格式变成统一快照格式。

调用图:被 1 处调用(parse_rate_limit_event)。

parse_promo_message177–182 ↗
fn parse_promo_message(headers: &HeaderMap) -> Option<String>

作用:从响应头里读取促销或提示文字。比如服务器想告诉用户“升级套餐可获得更多额度”,这类消息会从这里取出。

数据流:进去的是 HTTP 响应头 → 它读取 x-codex-promo-message → 去掉前后空白,空内容不要 → 输出一段字符串,或者没有消息就输出空。

调用关系:它使用 parse_header_str 读取原始响应头文字。它和限额解析一样属于响应处理阶段,但只关心给用户看的提示文本。

调用图:调用 1 个内部函数(parse_header_str)。

parse_rate_limit_reached_type184–189 ↗
fn parse_rate_limit_reached_type(headers: &HeaderMap) -> Option<RateLimitReachedType>

作用:读取“到底是哪种限额被撞到了”。这能帮助客户端区分是主限额、次限额、点数还是其它原因导致请求受限。

数据流:进去的是 HTTP 响应头 → 它读取 x-codex-rate-limit-reached-type → 去空白后尝试解析成 RateLimitReachedType → 成功就输出类型,失败或没有响应头就输出空。

调用关系:它使用 parse_header_str 拿到原始字符串,再交给类型自己的解析逻辑。通常在收到限额错误响应时,用它给用户或上层逻辑更准确的原因。

调用图:调用 1 个内部函数(parse_header_str)。

parse_rate_limit_window191–213 ↗
fn parse_rate_limit_window(
    headers: &HeaderMap,
    used_percent_header: &str,
    window_minutes_header: &str,
    resets_at_header: &str,
) -> Option<RateLimitWindow>

作用:读取一个限额时间窗口的信息。这里的窗口可以理解成“这段时间内你用了多少额度,多久后重置”。

数据流:进去的是响应头,以及三个响应头名字:使用百分比、窗口分钟数、重置时间 → 它先读使用百分比,再读窗口长度和恢复时间 → 如果全是空或没有有效数据,就输出空;否则输出 RateLimitWindow。

调用关系:parse_rate_limit_for_limit 会用它分别读取 primary 和 secondary 两个窗口。它把具体的数字读取交给 parse_header_f64 等底层小函数,自己负责判断这些数字是否足以组成一个有效窗口。

调用图:调用 1 个内部函数(parse_header_f64);被 1 处调用(parse_rate_limit_for_limit)。

parse_credits_snapshot215–227 ↗
fn parse_credits_snapshot(headers: &HeaderMap) -> Option<CreditsSnapshot>

作用:读取用户点数余额状态。点数就像账户里的余额,客户端需要知道还有没有、是不是无限、余额文字是什么。

数据流:进去的是 HTTP 响应头 → 它读取 has-credits 和 unlimited 两个布尔值,再读取可选的 balance 字符串 → 两个关键布尔值都有效时,输出 CreditsSnapshot;缺一个或格式不对就输出空。

调用关系:parse_rate_limit_for_limit 会调用它,把点数信息塞进限额快照。它依赖 parse_header_bool 和 parse_header_str 做具体响应头读取。

调用图:调用 2 个内部函数(parse_header_bool, parse_header_str);被 1 处调用(parse_rate_limit_for_limit)。

parse_header_f64229–234 ↗
fn parse_header_f64(headers: &HeaderMap, name: &str) -> Option<f64>

作用:把某个响应头读成小数。限额使用百分比可能是 12.5 这样的值,所以需要这个函数安全转换。

数据流:进去的是响应头集合和响应头名字 → 它先取出字符串,再尝试解析成 f64 小数 → 只有解析成功且不是无穷大、不是非法数字时才输出这个小数。

调用关系:parse_rate_limit_window 会用它读取 used-percent。它底层依赖 parse_header_str,先把响应头安全地变成文字。

调用图:调用 1 个内部函数(parse_header_str);被 1 处调用(parse_rate_limit_window)。

parse_header_i64236–238 ↗
fn parse_header_i64(headers: &HeaderMap, name: &str) -> Option<i64>

作用:把某个响应头读成整数。窗口分钟数和重置时间戳通常都是整数,需要用它来安全转换。

数据流:进去的是响应头集合和响应头名字 → 它先读取字符串 → 尝试解析成 64 位整数 → 成功就输出整数,失败就输出空。

调用关系:它是响应头数字解析的小工具,和 parse_header_f64、parse_header_bool 属于同一层。它通过 parse_header_str 先拿到原始文字。

调用图:调用 1 个内部函数(parse_header_str)。

parse_header_bool240–249 ↗
fn parse_header_bool(headers: &HeaderMap, name: &str) -> Option<bool>

作用:把某个响应头读成真假值。它同时接受 true/false 和 1/0,兼容服务器可能使用的两种写法。

数据流:进去的是响应头集合和响应头名字 → 它读取原始字符串 → true 或 1 变成 true,false 或 0 变成 false → 其它内容都当作无效,输出空。

调用关系:parse_credits_snapshot 会用它读取 has-credits 和 unlimited。它底层同样依赖 parse_header_str。

调用图:调用 1 个内部函数(parse_header_str);被 1 处调用(parse_credits_snapshot)。

parse_header_str251–253 ↗
fn parse_header_str(headers: &'a HeaderMap, name: &str) -> Option<&'a str>

作用:安全地读取某个响应头的文字内容。这是所有响应头解析的地基,先保证响应头存在且能当作正常字符串看。

数据流:进去的是响应头集合和响应头名字 → 它按名字查找响应头 → 如果找到了,并且内容能转换成字符串,就输出字符串引用;否则输出空。

调用关系:很多上层解析函数都靠它起步,包括点数、布尔值、小数、整数、促销消息、限额名称和触顶类型解析。它自己只负责最基础的读取,不判断业务含义。

调用图:被 7 处调用(parse_credits_snapshot, parse_header_bool, parse_header_f64, parse_header_i64, parse_promo_message, parse_rate_limit_for_limit, parse_rate_limit_reached_type);外部调用 1 个(get)。

has_rate_limit_data255–257 ↗
fn has_rate_limit_data(snapshot: &RateLimitSnapshot) -> bool

作用:判断一个限额快照里是不是真的带了有用信息。这样扫描多个限额时,不会把完全空的限额也塞进更新列表。

数据流:进去的是一个 RateLimitSnapshot → 它检查 primary、secondary、credits 三块是否至少有一块存在 → 输出 true 或 false,不改动快照。

调用关系:parse_all_rate_limits 在解析非默认限额时会用到这个判断。默认 codex 快照会保留,但其它限额如果没有实际数据,就会被过滤掉。

header_name_to_limit_id259–264 ↗
fn header_name_to_limit_id(header_name: &str) -> Option<String>

作用:从响应头名字里反推出限额编号。比如看到 x-codex-secondary-primary-used-percent,就知道这属于 codex_secondary 这组限额。

数据流:进去的是一个已经小写化的响应头名字 → 它要求名字以 x- 开头、以 -primary-used-percent 结尾 → 中间那段拿出来并统一格式 → 输出限额编号;不符合模式就输出空。

调用关系:parse_all_rate_limits 遍历所有响应头时会调用它,用来发现服务器返回了哪些限额族。它把最终格式整理交给 normalize_limit_id。

调用图:调用 1 个内部函数(normalize_limit_id);被 1 处调用(parse_all_rate_limits)。

normalize_limit_id266–268 ↗
fn normalize_limit_id(name: impl Into<String>) -> String

作用:统一限额编号的写法。它把名字去掉前后空格、转成小写,并把短横线换成下划线,避免同一个名字因为写法不同变成两个。

数据流:进去的是一个可转换成字符串的名字 → 它转成字符串、修剪空白、小写化、替换符号 → 输出标准化后的限额编号。

调用关系:header_name_to_limit_id 和 parse_rate_limit_for_limit 都会用它。前者从响应头名提取编号后标准化,后者在生成快照时保证 limit_id 稳定一致。

调用图:被 2 处调用(header_name_to_limit_id, parse_rate_limit_for_limit);外部调用 1 个(into)。

tests::parse_rate_limit_for_limit_defaults_to_codex_headers277–299 ↗
fn parse_rate_limit_for_limit_defaults_to_codex_headers()

作用:测试默认情况下会读取 codex 这组响应头。它保证不传限额编号时,程序不会漏掉最基础的限额信息。

数据流:进去的是测试里手工创建的响应头,包含默认 primary 使用比例、窗口分钟数和重置时间 → 它调用 parse_rate_limit_for_limit → 检查输出的 limit_id、primary 数值都符合预期。

调用关系:这是 parse_rate_limit_for_limit 的单元测试。它用固定响应头模拟服务器返回,再用断言确认解析结果没有偏。

调用图:调用 1 个内部函数(parse_rate_limit_for_limit);外部调用 3 个(new, from_static, assert_eq!)。

tests::parse_rate_limit_for_limit_reads_secondary_headers302–326 ↗
fn parse_rate_limit_for_limit_reads_secondary_headers()

作用:测试指定非默认限额时,函数会去读对应前缀的响应头。这样 codex_secondary 之类的细分限额不会被错当成默认 codex。

数据流:进去的是测试构造的 x-codex-secondary-primary-* 响应头 → 它传入 codex_secondary 调用解析函数 → 输出应当带有 codex_secondary 的 limit_id 和正确的 primary 窗口。

调用关系:它直接验证 parse_rate_limit_for_limit 对自定义限额编号的支持。测试结果能防止以后改响应头拼接规则时破坏兼容性。

调用图:调用 1 个内部函数(parse_rate_limit_for_limit);外部调用 3 个(new, from_static, assert_eq!)。

tests::parse_rate_limit_for_limit_prefers_limit_name_header329–344 ↗
fn parse_rate_limit_for_limit_prefers_limit_name_header()

作用:测试服务器给出的显示名称会被保留下来。限额编号可能是内部名字,而 limit-name 更像给人看的模型或套餐名字。

数据流:进去的是包含使用比例和 x-codex-bengalfox-limit-name 的响应头 → 它调用 parse_rate_limit_for_limit → 检查输出里 limit_id 被标准化,同时 limit_name 等于响应头里的显示名称。

调用关系:它覆盖 parse_rate_limit_for_limit 读取 limit-name 响应头的分支,确保快照里不仅有机器用的编号,也有用户界面可能要展示的名字。

调用图:调用 1 个内部函数(parse_rate_limit_for_limit);外部调用 3 个(new, from_static, assert_eq!)。

tests::parse_all_rate_limits_reads_all_limit_families347–364 ↗
fn parse_all_rate_limits_reads_all_limit_families()

作用:测试一次扫描能同时读到默认限额和额外限额。它保证服务器返回多组限额时,客户端不会只看第一组。

数据流:进去的是同时含有 x-codex-primary-used-percent 和 x-codex-secondary-primary-used-percent 的响应头 → 它调用 parse_all_rate_limits → 检查结果有两条,分别是 codex 和 codex_secondary。

调用关系:这是 parse_all_rate_limits 的关键测试。它间接验证了默认解析、响应头名扫描、header_name_to_limit_id 识别限额编号这些步骤能配合起来。

调用图:调用 1 个内部函数(parse_all_rate_limits);外部调用 3 个(new, from_static, assert_eq!)。

tests::parse_all_rate_limits_includes_default_codex_snapshot367–377 ↗
fn parse_all_rate_limits_includes_default_codex_snapshot()

作用:测试即使没有任何限额响应头,也会返回一条默认 codex 快照。这样上层代码可以稳定地看到默认限额槽位,而不是每次都先判断有没有列表。

数据流:进去的是空的响应头集合 → 它调用 parse_all_rate_limits → 检查结果只有一条 codex 快照,并且窗口和点数信息都是空。

调用关系:它验证 parse_all_rate_limits 开头调用 parse_default_rate_limit 的行为。这个测试防止未来有人为了过滤空数据,把默认 codex 快照也误删掉。

调用图:调用 1 个内部函数(parse_all_rate_limits);外部调用 2 个(new, assert_eq!)。

core/src/unified_exec/async_watcher.rs源码 ↗
orchestrationrequest handling / process lifetime

可以把这个文件想成“后台值班员”。统一执行命令启动后,它会派出两个后台任务:一个专门听命令吐出来的字节,把它们存进一份共享记录里,并按安全的小块发出实时输出事件;另一个等进程退出,再等最后一点输出也流完,然后发出一次“命令结束”的事件。这里特别小心两件事:第一,输出可能不是一次正好组成完整文字,所以它会尽量按 UTF-8(常见文字编码)边界切开,避免把中文或 emoji 切坏;第二,每次发出的输出块有大小上限,避免下游收到特别大的消息被拖垮。最后结果优先使用保存下来的 transcript(输出记录),如果没有记录才用备用文本。

函数细节8
start_streaming_output40–102 ↗
fn start_streaming_output(
    process: &UnifiedExecProcess,
    context: &UnifiedExecContext,
    transcript: Arc<Mutex<HeadTailBuffer>>,
)

作用:启动一个后台任务,持续读取命令的实时输出,并把这些输出发成“增量事件”。这样用户不用等命令完全结束,就能看到它正在打印什么。

数据流:进去的是正在运行的进程、执行上下文和共享输出记录 → 它从进程拿到输出接收器、退出信号和“输出已清空”的通知器,再复制会话、回合和调用编号 → 后台循环收到输出块后交给 process_chunk;如果进程退出,会多等 100 毫秒收尾输出,然后通知别人输出已经读完。

调用关系:它由 exec_command 在命令开始后调用,像是给新进程安排一个实时监听员。它会使用进程提供的 output_receiver、output_drained_notify 和 cancellation_token,并在 tokio 的后台任务里运行;真正处理每个输出块的细活交给 process_chunk。

调用图:调用 3 个内部函数(cancellation_token, output_drained_notify, output_receiver);被 1 处调用(exec_command);外部调用 4 个(clone, new, select!, spawn)。

spawn_exit_watcher107–157 ↗
fn spawn_exit_watcher(
    process: Arc<UnifiedExecProcess>,
    session_ref: Arc<Session>,
    turn_ref: Arc<TurnContext>,
    call_id: String,
    command: Vec<String>,
    cwd: AbsolutePathBuf,

作用:启动另一个后台任务,专门等命令结束,并在最后发出一次完整的结束事件。它保证“结束通知”不会早于最后一批输出。

数据流:进去的是进程、会话、回合、命令信息、工作目录、进程号、输出记录和开始时间 → 它等待进程的取消/退出信号,再等待 start_streaming_output 那边确认输出已经读干净 → 计算运行时长;如果进程有失败信息,就发失败结束事件,否则拿退出码发成功结束事件。

调用关系:它由 store_process 在保存进程后调用,负责进程生命周期的收尾。它根据进程状态选择把工作交给 emit_failed_exec_end_for_unified_exec 或 emit_exec_end_for_unified_exec。

调用图:调用 2 个内部函数(emit_exec_end_for_unified_exec, emit_failed_exec_end_for_unified_exec);被 1 处调用(store_process);外部调用 3 个(now, new, spawn)。

process_chunk159–189 ↗
async fn process_chunk(
    pending: &mut Vec<u8>,
    transcript: &Arc<Mutex<HeadTailBuffer>>,
    call_id: &str,
    session_ref: &Arc<Session>,
    turn_ref: &Arc<TurnContext>,
    emitted_deltas:

作用:处理刚从命令那里读到的一小段原始字节:保存到总记录里,并尽量立刻发给外部。它还会控制输出事件数量,防止一个命令刷屏太多。

数据流:进去的是尚未凑成完整文字的 pending 字节、共享 transcript、调用编号、会话、回合、已发送数量和新 chunk → 它把新字节接到 pending 后面,反复切出合法的 UTF-8 前缀 → 每切出一段就写入 transcript;如果还没超过每次调用允许的增量事件上限,就包装成 ExecCommandOutputDeltaEvent 发给会话,并增加计数。

调用关系:它是在 start_streaming_output 创建的后台读取循环里被使用的“小工序”。它依赖 split_valid_utf8_prefix 来安全切块,然后通过会话发送 ExecCommandOutputDelta 事件,让外部界面能看到实时输出。

调用图:调用 1 个内部函数(split_valid_utf8_prefix);外部调用 1 个(ExecCommandOutputDelta)。

emit_exec_end_for_unified_exec195–237 ↗
async fn emit_exec_end_for_unified_exec(
    session_ref: Arc<Session>,
    turn_ref: Arc<TurnContext>,
    call_id: String,
    command: Vec<String>,
    cwd: AbsolutePathBuf,
    process_id: Option<

作用:发送一次“命令成功结束”的事件,里面带上退出码、运行时长和汇总输出。外部系统靠这个知道命令已经正常收场。

数据流:进去的是会话、回合、调用编号、命令、目录、进程号、输出记录、备用输出、退出码和耗时 → 它先用 resolve_aggregated_output 得到最终汇总输出 → 组装 ExecToolCallOutput,把 stdout、aggregated_output、退出码和耗时放进去 → 通过 ToolEmitter 作为成功阶段发出事件。

调用关系:它会被 spawn_exit_watcher 在进程正常结束时调用,也可能被 exec_command 在某些直接收尾场景调用。它把“内部收集到的进程结果”转换成工具事件系统能理解的成功事件。

调用图:调用 4 个内部函数(unified_exec, new, resolve_aggregated_output, new);被 2 处调用(spawn_exit_watcher, exec_command);外部调用 1 个(new)。

emit_failed_exec_end_for_unified_exec240–288 ↗
async fn emit_failed_exec_end_for_unified_exec(
    session_ref: Arc<Session>,
    turn_ref: Arc<TurnContext>,
    call_id: String,
    command: Vec<String>,
    cwd: AbsolutePathBuf,
    process_id:

作用:发送一次“命令失败结束”的事件,里面带上已经拿到的输出和失败原因。这样外部不仅知道失败了,还能看到失败前打印过什么。

数据流:进去的是会话、回合、调用编号、命令、目录、进程号、输出记录、备用输出、失败消息和耗时 → 它决定 stdout 用备用输出还是 transcript;再把失败消息放进 stderr,并把 stdout 和错误消息合成 aggregated_output → 组装 exit_code 为 -1 的 ExecToolCallOutput → 通过 ToolEmitter 作为失败阶段发出事件。

调用关系:它会被 spawn_exit_watcher 在进程报告失败时调用,也会被 emit_failed_initial_exec_end_if_unstored 用来处理还没存好进程就失败的情况。它负责把失败细节变成统一的工具失败事件。

调用图:调用 4 个内部函数(unified_exec, new, resolve_aggregated_output, new);被 2 处调用(spawn_exit_watcher, emit_failed_initial_exec_end_if_unstored);外部调用 3 个(Output, Failure, format!)。

split_valid_utf8_prefix290–292 ↗
fn split_valid_utf8_prefix(buffer: &mut Vec<u8>) -> Option<Vec<u8>>

作用:从一堆字节开头切出一段可以安全当文字发送的内容。它是一个简单包装,使用文件里统一规定的最大输出块大小。

数据流:进去的是可变的字节缓冲区 → 它把缓冲区和固定的最大字节数交给 split_valid_utf8_prefix_with_max → 出来的是一段已从缓冲区移走的字节,或者没有可切内容。

调用关系:它被 process_chunk 调用,处在实时输出切块的关键位置。它把“按默认大小安全切文字”的规则集中起来,避免 process_chunk 直接关心具体上限。

调用图:调用 1 个内部函数(split_valid_utf8_prefix_with_max);被 1 处调用(process_chunk)。

split_valid_utf8_prefix_with_max294–318 ↗
fn split_valid_utf8_prefix_with_max(buffer: &mut Vec<u8>, max_bytes: usize) -> Option<Vec<u8>>

作用:按给定最大大小,从缓冲区前面切出一段尽量完整的 UTF-8 文字。UTF-8 可以理解为一种文字存法,一个字可能占多个字节,所以不能随便从中间劈开。

数据流:进去的是可变字节缓冲区和最大字节数 → 它从不超过最大值的位置开始往前试,找到能被 from_utf8 认作合法文字的一段,就取出并从原缓冲区删除 → 如果实在找不到合法开头,就取出第一个字节,保证流还能继续前进,不会卡死。

调用关系:它由 split_valid_utf8_prefix 调用,是实际切块算法所在。process_chunk 间接依赖它,保证发出去的输出块尽量不破坏文字,同时又不会因为坏字节永远停住。

调用图:被 1 处调用(split_valid_utf8_prefix);外部调用 1 个(from_utf8)。

resolve_aggregated_output320–330 ↗
async fn resolve_aggregated_output(
    transcript: &Arc<Mutex<HeadTailBuffer>>,
    fallback: String,
) -> String

作用:决定最终汇总输出该用哪一份:优先用已经保存的 transcript,如果 transcript 为空才用备用文本。

数据流:进去的是共享 transcript 和 fallback 字符串 → 它先给 transcript 加锁读取,查看里面有没有保留字节 → 如果没有,就返回 fallback;如果有,就把 transcript 字节用宽容方式转成字符串,遇到不完整或非法文字也尽量保留可读内容。

调用关系:它被 emit_exec_end_for_unified_exec 和 emit_failed_exec_end_for_unified_exec 调用,处在最终事件生成前。它把实时阶段积攒的输出记录变成结束事件里的 aggregated_output。

调用图:被 2 处调用(emit_exec_end_for_unified_exec, emit_failed_exec_end_for_unified_exec);外部调用 1 个(from_utf8_lossy)。

exec/src/event_processor_with_human_output.rs源码 ↗
io_transportmain loop 和 shutdown

这个文件像一个“现场解说员”。Codex 在运行时会不断收到服务器通知,比如配置好了、开始执行命令、命令成功或失败、模型写了回答、补丁应用了、用了多少 token。这里的 EventProcessorWithHumanOutput 会把这些通知整理成适合人看的终端输出,并用颜色、粗体、灰字等 ANSI 样式让重点更清楚;如果终端不支持颜色,也能退回普通文字。它还会记住最后一条模型回答,必要时写到指定文件,或在程序结束时打印到标准输出。一个重要细节是:它会避免重复打印最终回答;如果回答已经在交互过程中显示过,退出时就不再刷一遍。它还区分 stdout(标准输出,常给脚本读取)和 stderr(标准错误,常给人看),这样既方便人读,也方便自动化工具接结果。

函数细节13
EventProcessorWithHumanOutput::create_with_ansi42–65 ↗
fn create_with_ansi(
        with_ansi: bool,
        config: &Config,
        last_message_path: Option<PathBuf>,
    ) -> Self

作用:创建一个面向人类终端的事件处理器,并决定要不要使用颜色和样式。别人开始运行一次 exec 会话时,会先用它准备好这个“输出解说员”。

数据流:输入是是否启用 ANSI 样式、当前配置、以及可选的“最后消息保存路径” → 它根据配置准备粗体、彩色、灰字等显示风格,并记住是否显示模型推理内容 → 输出一个新的 EventProcessorWithHumanOutput,初始时还没有最终回答,也没有 token 统计。

调用关系:run_exec_session 在启动执行会话时调用它,相当于先搭好终端显示器。它内部只用基础样式构造,不把活儿交给别的项目函数。

调用图:被 1 处调用(run_exec_session);外部调用 1 个(new)。

EventProcessorWithHumanOutput::render_item_started67–96 ↗
fn render_item_started(&self, item: &ThreadItem)

作用:当某个任务刚开始时,把“正在做什么”先告诉用户。比如开始跑命令、调用工具、搜索网页、应用补丁。

数据流:输入是一个 ThreadItem,也就是一次会话里的某个工作项 → 它按类型挑一句适合人看的提示,并带上命令、目录、工具名或搜索词 → 输出到 stderr,不返回新数据,也不改变主要状态。

调用关系:process_server_notification 收到 ItemStarted 通知时会调用它。它是开始阶段的小喇叭,只负责报幕,不决定任务成败。

调用图:被 1 处调用(process_server_notification);外部调用 1 个(eprintln!)。

EventProcessorWithHumanOutput::render_item_completed98–208 ↗
fn render_item_completed(&mut self, item: ThreadItem)

作用:当某个任务结束时,把结果讲清楚。它会显示模型回答、命令输出、补丁状态、工具错误等。

数据流:输入是已经完成的 ThreadItem → 它根据项目类型打印不同内容;如果是模型最终消息,会保存到 final_message 并标记已经显示过;如果是推理内容,会按配置决定显示摘要还是原始内容;如果是命令,会显示成功、失败、退出码和输出 → 结果主要是终端上出现说明,同时可能更新最终消息状态。

调用关系:process_server_notification 收到 ItemCompleted 通知时调用它。它会把推理文字的选择交给 reasoning_text,然后自己完成实际打印和状态记录。

调用图:调用 1 个内部函数(reasoning_text);被 1 处调用(process_server_notification);外部调用 1 个(eprintln!)。

EventProcessorWithHumanOutput::print_config_summary212–225 ↗
fn print_config_summary(
        &mut self,
        config: &Config,
        prompt: &str,
        session_configured_event: &SessionConfiguredEvent,
    )

作用:在会话开始时打印一页简短说明,让用户知道这次 Codex 会在哪个目录、用哪个模型、按什么权限运行。

数据流:输入是配置、用户提示词、以及服务器确认后的会话信息 → 它先拿到程序版本号,再调用 config_summary_entries 生成要展示的配置条目 → 输出到 stderr,包括标题、配置摘要和用户原始问题。

调用关系:这是 EventProcessor 接口的一部分,通常在会话刚配置完成后被上层调用。它把“该列哪些配置”的工作交给 config_summary_entries,自己负责排版和打印。

调用图:调用 1 个内部函数(config_summary_entries);外部调用 2 个(env!, eprintln!)。

EventProcessorWithHumanOutput::process_server_notification227–367 ↗
fn process_server_notification(&mut self, notification: ServerNotification) -> CodexStatus

作用:这是本文件的核心入口:每收到一条服务器通知,就决定该显示什么,以及程序该继续跑还是准备退出。

数据流:输入是一条 ServerNotification,也就是服务器发来的事件 → 它按事件种类分流:警告就打印警告,开始任务就调用 render_item_started,完成任务就调用 render_item_completed,回合结束就保存最终回答并返回关闭信号,计划更新就打印步骤 → 输出 CodexStatus,告诉外层现在继续运行还是开始收尾,同时会更新最终消息、token 用量等内部状态。

调用关系:运行中的事件循环会反复调用它。它像总调度台,把具体显示工作交给 process_warning、render_item_started、render_item_completed、final_message_from_turn_items 等辅助函数。

调用图:调用 4 个内部函数(process_warning, render_item_completed, render_item_started, final_message_from_turn_items);外部调用 1 个(eprintln!)。

EventProcessorWithHumanOutput::process_warning369–375 ↗
fn process_warning(&mut self, message: String) -> CodexStatus

作用:把普通警告用醒目的格式打印出来。这样用户能知道有问题需要注意,但程序还可以继续运行。

数据流:输入是一段警告文字 → 它加上黄色加粗的 warning 标签并输出到 stderr → 返回 Running,表示只是提醒,不要求停机。

调用关系:process_server_notification 遇到 Warning 类型通知时会调用它。它是警告打印的统一小出口,避免每处都重复写同样格式。

调用图:被 1 处调用(process_server_notification);外部调用 1 个(eprintln!)。

EventProcessorWithHumanOutput::print_final_output377–417 ↗
fn print_final_output(&mut self)

作用:在程序准备结束时做最后收尾:保存最后回答、显示 token 用量,并决定最终回答该不该再打印一次。

数据流:它读取内部保存的 final_message、last_message_path、token 使用量,以及 stdout/stderr 是否连接到终端 → 如果需要,会把最后回答写到文件;会打印本次实际消耗的 token;还会根据当前是不是交互终端,决定把最终回答打印到 stdout 给脚本用,还是打印到 stderr 给人看,或者不重复打印 → 输出是终端文字和可能的文件更新。

调用关系:外层在收到关闭信号后调用它。它把写最后消息交给 handle_last_message,把“该打印到 stdout 还是 tty”的判断交给 should_print_final_message_to_stdout 和 should_print_final_message_to_tty。

调用图:调用 3 个内部函数(handle_last_message, should_print_final_message_to_stdout, should_print_final_message_to_tty);外部调用 4 个(eprintln!, println!, stderr, stdout)。

config_summary_entries420–467 ↗
fn config_summary_entries(
    config: &Config,
    session_configured_event: &SessionConfiguredEvent,
) -> Vec<(&'static str, String)>

作用:整理会话开头要展示的配置清单。它把分散在配置和服务器确认信息里的内容,凑成一组“名称和值”。

数据流:输入是本地 Config 和 SessionConfiguredEvent → 它读取工作目录、模型、服务商、审批策略、沙箱权限、会话 ID;如果使用 Responses 接口,还会加入推理强度和推理摘要设置 → 输出一个列表,供打印函数逐行显示。

调用关系:print_config_summary 会调用它来拿展示内容。它不负责打印,只负责把配置变成适合展示的条目。

调用图:被 1 处调用(print_config_summary);外部调用 1 个(vec!)。

reasoning_text469–484 ↗
fn reasoning_text(
    summary: &[String],
    content: &[String],
    show_raw_agent_reasoning: bool,
) -> Option<String>

作用:决定要显示哪种模型推理文字。配置允许时可以显示原始推理,否则只显示摘要;如果什么都没有,就不显示。

数据流:输入是推理摘要列表、原始推理内容列表、以及是否显示原始推理的开关 → 它先选择原始内容或摘要,再把多段文字用换行拼起来 → 输出一段可打印文字,或者在没有内容时输出 None。

调用关系:render_item_completed 在处理 Reasoning 项时调用它。它只做内容选择,不处理颜色和打印。

调用图:被 1 处调用(render_item_completed)。

final_message_from_turn_items486–500 ↗
fn final_message_from_turn_items(items: &[ThreadItem]) -> Option<String>

作用:从一个回合的所有项目里找出最适合作为“最终答案”的文字。优先找最后一条模型回答;如果没有回答,就退而求其次找最后一份计划文本。

数据流:输入是一组 ThreadItem → 它从后往前查找 AgentMessage,找到就复制其中的 text;如果没找到,再从后往前找 Plan → 输出最终消息字符串,或者没有合适内容时输出 None。

调用关系:process_server_notification 在收到 TurnCompleted 且回合成功时调用它。它帮助收尾逻辑确认最终应该保存和可能打印哪段文字。

调用图:被 1 处调用(process_server_notification);外部调用 1 个(iter)。

blended_total502–506 ↗
fn blended_total(usage: &ThreadTokenUsage) -> i64

作用:计算一个更适合展示给用户的 token 总量。token 可以粗略理解为模型读写文字时使用的“计量单位”。

数据流:输入是 ThreadTokenUsage → 它把已缓存的输入 token 从输入总数里扣掉,再加上输出 token,并把负数保护成 0 → 输出一个整数,表示本次非缓存输入加输出的大致用量。

调用关系:print_final_output 在显示 tokens used 时使用这个计算结果。它不打印,只负责算数。

should_print_final_message_to_stdout508–514 ↗
fn should_print_final_message_to_stdout(
    final_message: Option<&str>,
    stdout_is_terminal: bool,
    stderr_is_terminal: bool,
) -> bool

作用:判断最终回答是否应该打印到 stdout。stdout 是标准输出,常被脚本或管道拿来读取程序结果。

数据流:输入是有没有最终回答、stdout 是否是终端、stderr 是否是终端 → 如果有最终回答,并且当前不是 stdout 和 stderr 都面向真人终端的交互场景,就返回 true → 输出一个布尔值,告诉收尾函数要不要把答案给 stdout。

调用关系:print_final_output 调用它来避免人机交互时重复刷屏,同时保证脚本调用时能拿到干净的最终答案。

调用图:被 1 处调用(print_final_output)。

should_print_final_message_to_tty516–523 ↗
fn should_print_final_message_to_tty(
    final_message: Option<&str>,
    final_message_rendered: bool,
    stdout_is_terminal: bool,
    stderr_is_terminal: bool,
) -> bool

作用:判断最终回答是否应该在交互终端里补打一遍。TTY 可以理解为真正有人正在看的终端窗口。

数据流:输入是有没有最终回答、这条回答之前是否已经显示过、stdout/stderr 是否都是终端 → 如果有回答、之前没显示过,并且当前确实是交互终端,就返回 true → 输出一个布尔值,告诉收尾函数是否要把答案打印给人看。

调用关系:print_final_output 调用它处理另一种情况:模型最终答案还没在过程中显示,但用户在终端里等结果,所以退出前需要补上。

调用图:被 1 处调用(print_final_output)。

ollama/src/parser.rs源码 ↗
io_transportrequest handling / stream parsing

拉取模型时,服务端会不断发回一些 JSON 对象,比如“正在校验”“下载了多少”“成功了”。JSON 很灵活,但也容易让后面的代码写得乱:每个地方都去读 status、digest、total、completed,就像每个人都自己拆快递单,容易看错。这个文件的核心函数 pull_events_from_value 就是统一的“翻译员”。它先看 JSON 里有没有 status,有就生成一个状态事件;如果状态正好是 success,还会额外生成一个成功事件。然后它再看有没有 total 或 completed,只要出现进度数字,就生成一个分块进度事件,并带上 digest,也就是这块内容的身份标记。文件下面的测试用几个小 JSON 检查翻译是否符合预期,避免以后改代码时把成功、状态或进度的含义弄错。

函数细节3
pull_events_from_value6–29 ↗
fn pull_events_from_value(value: &JsonValue) -> Vec<PullEvent>

作用:把一个 JSON 对象转换成一个或多个 PullEvent,也就是程序内部表示“拉取进展”的事件。别人用它时,不必直接处理原始 JSON 字段,只要接收清楚的状态、成功或进度事件。

数据流:输入是一段 serde_json::Value,也就是已经解析好的 JSON 数据。函数读取里面的 status、digest、total、completed:有 status 就产出状态事件;status 等于 success 时再产出成功事件;有 total 或 completed 时产出下载进度事件。输出是一个 PullEvent 列表,函数本身不改外部数据,只根据输入整理出新的事件。

调用关系:它是这个文件的核心翻译步骤。测试函数 tests::test_pull_events_decoder_status_and_success 和 tests::test_pull_events_decoder_progress 会直接调用它,用不同形状的 JSON 验证它能正确拆出状态、成功和进度事件。函数内部用 JSON 的 get 方法取字段,并构造 PullEvent::Status 等事件。

调用图:被 2 处调用(test_pull_events_decoder_progress, test_pull_events_decoder_status_and_success);外部调用 3 个(get, new, Status)。

tests::test_pull_events_decoder_status_and_success38–48 ↗
fn test_pull_events_decoder_status_and_success()

作用:这个测试确认状态类 JSON 能被正确翻译,尤其是 success 这个特殊状态会同时产生“状态是 success”和“已经成功”两个事件。

数据流:它先造出一个包含 status: verifying 的 JSON,送进 pull_events_from_value,检查结果只有一个状态事件。接着造出 status: success 的 JSON,再送进去,检查结果有两个事件:第一个说明状态文字是 success,第二个说明拉取已经成功。

调用关系:它在测试阶段运行,是 pull_events_from_value 的安全网之一。它把手工构造的 JSON 交给核心解析函数,然后用 assert_matches! 和 assert_eq! 检查输出,确保状态和成功信号不会被未来的改动破坏。

调用图:调用 1 个内部函数(pull_events_from_value);外部调用 3 个(assert_eq!, assert_matches!, json!)。

tests::test_pull_events_decoder_progress51–74 ↗
fn test_pull_events_decoder_progress()

作用:这个测试确认进度类 JSON 能被正确翻译,包括只有 total 或只有 completed 的情况。这样可以保证服务端只给部分进度信息时,程序也不会误判。

数据流:它先造出带 digest 和 total 的 JSON,送进 pull_events_from_value,检查输出是一个进度事件,并且 total 是 100、completed 为空。然后再造出带 digest 和 completed 的 JSON,检查输出同样是进度事件,但这次 completed 是 42、total 为空。

调用关系:它也是围绕 pull_events_from_value 的测试。它专门覆盖进度字段的组合情况,帮助保证解析函数面对不完整但合法的进度消息时,仍能把已有信息原样传给后续流程。

调用图:调用 1 个内部函数(pull_events_from_value);外部调用 3 个(assert_eq!, assert_matches!, json!)。