Codex 系统手册

结果持久化、展示与状态更新

stage-1655 个文件

这一阶段像“记账和播报员”,发生在每轮工作推进或结束后。核心先把模型回复、工具结果、文件改动整理成统一事件,再筛选哪些要写进会话记录和状态库;同时更新线程状态、标题、用量、归档等信息。服务器把内部事件翻译成前端通知,重连时还能补回历史。TUI 和 CLI 再把这些记录变成状态栏、聊天历史、diff、体检报告;exec 则输出 JSON,方便外部程序读取。

本阶段涉及的状态31
  • reg-auth-account用户当前是谁、有没有登录、用的是哪种账号或套餐、令牌是否还能用的身份状态。
  • reg-account-quota-limits账号的额度、限流、消费控制和剩余可用量信息,用来决定请求能不能继续发。
  • reg-local-state-runtime本地 SQLite 和 StateRuntime 打开的那套持久化句柄,负责让线程、日志、目标、记忆等数据能长期保存。
  • reg-thread-session-store当前有哪些线程和会话、哪个正在用、哪些能恢复或关闭的共享会话账本。
  • reg-conversation-history每条线程里的用户消息、模型回复、工具调用、文件改动和审批记录等对话历史。
  • reg-agent-graph多智能体和 fork 线程之间谁带起谁、父子关系是否还活着的关系图。
  • reg-plugin-registry已安装、可用、来自市场或本地的插件清单,以及每个插件的能力和加载结果。
  • reg-memory-store系统保存和检索长期记忆的地方,包含记忆文件、目录、搜索结果和后台整理状态。
  • reg-permissions-approvals用户已经允许、拒绝或需要确认的命令、文件、联网、MCP 调用等权限和审批状态。
  • reg-exec-environment-processesexec-server 和统一执行层里正在运行或已经结束的进程、退出码、输入输出和失败原因。
  • reg-transport-rpc-connectionsstdio、本机 socket、WebSocket、JSON-RPC、relay 等通道上的连接、请求编号和收发队列。
  • reg-server-daemon-stateapp-server、daemon、exec-server 等常驻服务当前是否启动、有哪些客户端连着、处理器是否还接单的运行状态。
  • reg-ui-interaction-stateTUI 或前端当前显示哪个线程、输入框内容、弹窗、快捷键、通知和终端接管等界面状态。
  • reg-turn-execution-state一轮对话正在排队、运行、取消、追加输入、压缩上下文或等待工具结果的临时但跨模块共享状态。
  • reg-hook-lifecycle-state钩子在配置、回合、工具调用和审批过程里会触发什么、传什么上下文、返回什么结果的共享状态。
  • reg-observability-telemetry日志、指标、分析事件、请求耗时、工具结果和错误报告等观测数据的收集与发送状态。
  • reg-cloud-task-state云端任务、任务列表、默认环境、尝试次数、状态和结果在本地界面与后端之间同步的那份状态。
  • reg-code-mode-sessions代码模式运行器里的会话、正在执行的脚本、等待结果、定时器和宿主回调状态。
  • reg-rollout-trace-logrollout 记录下来的可回放流水账和会话状态,用来恢复、排错、重连和事后分析。
  • reg-goal-store系统当前保存和检索的目标、任务意图和目标相关上下文,用于在线程运行、提示词组装和结果更新时持续引用。
  • reg-context-token-budget每轮组装上下文和调用模型时可用的上下文窗口、剩余 token 预算和截断压缩决策状态。
  • reg-agent-orchestration-runtime多智能体运行时的活跃代理登记、并发闸门、等待区、代理间消息和等待结果状态。
  • reg-feedback-report-buffer用户反馈、附带日志线索、脱敏后的诊断包以及上传重试/完成状态的暂存缓冲。
  • reg-review-workflow-state代码审查请求的目标、审查要求、辅助审查代理进度、发现项和最终审查结果状态。
  • reg-external-import-state外部聊天记录或会话导入时的来源登记、去重映射、转换进度和已导入标记。
  • reg-worktree-diff-state当前工作树快照、Git 状态、文件改动摘要和待展示或持久化的 diff 状态。
  • reg-active-model-selection当前线程或会话实际选中的模型、提供方和推理参数状态,区别于仅列出可用模型的模型目录。
  • reg-active-workspace-context当前会话选中的工作目录、项目根和能力根等工作区上下文,用于提示词、工具执行、权限判断和结果展示。
  • reg-session-usage-accounting每个线程或回合累计的模型 token、请求次数、工具用量和成本/用量统计状态,用于展示、持久化和遥测。
  • reg-onboarding-preferences-state新手引导、提示是否已看过、界面偏好和类似本地用户体验标记的持久状态。
  • reg-batch-dispatch-stateCSV 等批量派工流程中的输入批次、逐项代理分配、执行进度、失败项和汇总结果状态。

本阶段的文件55

App-server 事件投射

这些文件将核心执行事件转换为 app-server 通知、线程历史投射、重放的用量更新,以及外部可见的线程状态。

app-server/src/bespoke_event_handling.rs源码 ↗
orchestrationmain loop / request handling

核心引擎会不断发出事件:一轮对话开始了、命令要执行了、需要用户批准文件改动了、模型报错了、token 用量更新了等等。客户端不能直接吃这些内部事件,所以这个文件像前台接待一样,把内部说法翻译成 app-server 协议里的通知;遇到需要用户选择的事,还会发起一个“服务器请求”,等客户端回答后再把结果提交回核心引擎。它也会维护一些线程内状态,比如当前轮有没有错误、某个命令开始通知是否已经发过,避免重复显示。这里还特别处理了打断、回滚、实时语音、MCP 工具、权限申请、Guardian 安全审查等边角流程。没有它,前端会看不到正确进度,审批结果也回不到核心,甚至一次命令可能被显示两遍或永远卡住。

函数细节63
apply_bespoke_event_handling137–1277 ↗
async fn apply_bespoke_event_handling(
    event: Event,
    conversation_id: ThreadId,
    conversation: Arc<CodexThread>,
    thread_manager: Arc<ThreadManager>,
    outgoing: ThreadScopedOutgoingMe

作用:这是本文件的总入口。每当核心引擎发来一个事件,它就判断事件类型,然后发通知、发审批请求、更新线程状态,或把客户端回答再送回核心。

数据流:进去的是一个核心事件、线程编号、核心线程对象、发送器和线程状态等信息 → 它按事件种类拆开处理,比如 TurnStarted 变成 TurnStarted 通知,ExecApprovalRequest 变成命令审批请求,Error 变成错误通知 → 出来的是发给客户端的消息、提交给核心的操作,以及被更新的线程状态。

调用关系:它是整条事件处理链的调度中心。测试里的 apply_guardian_assessment_event、interrupted_subagent_activity_removes_missing_thread_watch、turn_started_omits_active_snapshot_items 会直接调用它;它再把具体小活交给 complete_command_execution_item、handle_error_notification、handle_turn_complete、handle_token_count_event 等辅助函数。

调用图:调用 38 个内部函数(complete_command_execution_item, handle_error_notification, handle_thread_rollback_failed, handle_token_count_event, handle_turn_complete, handle_turn_diff, handle_turn_interrupted, handle_turn_plan_update, maybe_emit_hook_prompt_item_completed, maybe_emit_raw_response_item_completed (+15 more));被 3 处调用(apply_guardian_assessment_event, interrupted_subagent_activity_removes_missing_thread_watch, turn_started_omits_active_snapshot_items);外部调用 46 个(DeprecationNotice, Error, GuardianWarning, HookCompleted, HookStarted, ItemCompleted, ItemStarted, McpServerStatusUpdated, ModelRerouted, ModelVerification (+15 more))。

handle_turn_diff1279–1293 ↗
async fn handle_turn_diff(
    conversation_id: ThreadId,
    event_turn_id: &str,
    turn_diff_event: TurnDiffEvent,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:把一轮对话里产生的代码差异内容发给客户端。这样前端能及时展示“这轮改了哪些文件”。

数据流:进去的是线程编号、轮次编号和一段统一 diff 文本 → 它包装成 TurnDiffUpdated 通知 → 发给当前线程的客户端。

调用关系:apply_bespoke_event_handling 遇到 TurnDiff 事件时会调用它;测试 test_handle_turn_diff_emits_v2_notification 单独检查它发出的通知格式。

调用图:调用 1 个内部函数(send_server_notification);被 2 处调用(apply_bespoke_event_handling, test_handle_turn_diff_emits_v2_notification);外部调用 2 个(TurnDiffUpdated, to_string)。

handle_turn_plan_update1295–1315 ↗
async fn handle_turn_plan_update(
    conversation_id: ThreadId,
    event_turn_id: &str,
    plan_update_event: UpdatePlanArgs,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:把模型更新的待办计划发给客户端。这里的计划是清单式步骤,不是“计划模式”的内部状态。

数据流:进去的是线程编号、轮次编号和计划内容 → 它把核心里的步骤转换成客户端协议里的 TurnPlanStep → 发出 TurnPlanUpdated 通知。

调用关系:apply_bespoke_event_handling 收到 PlanUpdate 时会交给它;测试 test_handle_turn_plan_update_emits_notification_for_v2 验证前端能收到正确计划。

调用图:调用 1 个内部函数(send_server_notification);被 2 处调用(apply_bespoke_event_handling, test_handle_turn_plan_update_emits_notification_for_v2);外部调用 2 个(TurnPlanUpdated, to_string)。

emit_turn_completed_with_status1325–1347 ↗
async fn emit_turn_completed_with_status(
    conversation_id: ThreadId,
    event_turn_id: String,
    turn_completion_metadata: TurnCompletionMetadata,
    outgoing: &ThreadScopedOutgoingMessageSend

作用:统一发送“一轮对话结束了”的通知。调用者只要告诉它结束状态、错误和时间,它负责拼出客户端需要的完整消息。

数据流:进去的是线程编号、轮次编号和结束元信息 → 它创建一个不加载明细条目的 Turn 对象 → 发出 TurnCompleted 通知。

调用关系:handle_turn_complete 和 handle_turn_interrupted 都用它,避免完成和中断两条路各自手写一套通知格式。

调用图:调用 1 个内部函数(send_server_notification);被 2 处调用(handle_turn_complete, handle_turn_interrupted);外部调用 3 个(TurnCompleted, to_string, vec!)。

start_command_execution_item1350–1391 ↗
async fn start_command_execution_item(
    conversation_id: &ThreadId,
    turn_id: String,
    item_id: String,
    command: String,
    cwd: AbsolutePathBuf,
    command_actions: Vec<V2ParsedCommand

作用:通知客户端“有个命令开始执行或等待审批了”。它还会防重复,保证同一个命令的开始消息只发一次。

数据流:进去的是线程、轮次、命令编号、命令文本、工作目录、命令解析结果和发送器 → 它先把命令编号记到线程状态里;如果之前没记过,就发 ItemStarted 通知 → 返回 true 表示真的发了,false 表示这是重复开始。

调用关系:apply_bespoke_event_handling 在命令审批或 Guardian 审查开始时会用它;complete_command_execution_item 之后靠同一份状态判断是否该发结束通知;相关测试确认它不会重复发。

调用图:调用 2 个内部函数(now_unix_timestamp_ms, send_server_notification);被 3 处调用(apply_bespoke_event_handling, command_execution_started_helper_emits_once, complete_command_execution_item_emits_declined_once_for_pending_command);外部调用 2 个(ItemStarted, to_string)。

complete_command_execution_item1394–1438 ↗
async fn complete_command_execution_item(
    conversation_id: &ThreadId,
    turn_id: String,
    item_id: String,
    command: String,
    cwd: AbsolutePathBuf,
    process_id: Option<String>,
    s

作用:通知客户端“这个命令项结束了”,常用于命令被拒绝、失败或取消时收尾。它同样会防重复。

数据流:进去的是命令编号、命令信息、结束状态等 → 它先从线程状态里移除这个正在进行的命令;只有确实存在时才继续 → 发 ItemCompleted 通知。

调用关系:apply_bespoke_event_handling 和 on_command_execution_request_approval_response 会在审批拒绝或安全审查失败时调用它;测试确认同一个命令只完成一次。

调用图:调用 2 个内部函数(now_unix_timestamp_ms, send_server_notification);被 3 处调用(apply_bespoke_event_handling, on_command_execution_request_approval_response, complete_command_execution_item_emits_declined_once_for_pending_command);外部调用 2 个(ItemCompleted, to_string)。

maybe_emit_raw_response_item_completed1440–1454 ↗
async fn maybe_emit_raw_response_item_completed(
    conversation_id: ThreadId,
    turn_id: &str,
    item: codex_protocol::models::ResponseItem,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:把模型原始响应条目原样转发给客户端。名字里有 maybe,但当前实现总是会发。

数据流:进去的是线程编号、轮次编号和一个原始响应条目 → 它包装成 RawResponseItemCompleted 通知 → 发给客户端。

调用关系:apply_bespoke_event_handling 收到 RawResponseItem 时会调用它;同一路径还会先尝试 maybe_emit_hook_prompt_item_completed。

调用图:调用 1 个内部函数(send_server_notification);被 1 处调用(apply_bespoke_event_handling);外部调用 2 个(RawResponseItemCompleted, to_string)。

maybe_emit_hook_prompt_item_completed1456–1493 ↗
async fn maybe_emit_hook_prompt_item_completed(
    conversation_id: ThreadId,
    turn_id: &str,
    item: &codex_protocol::models::ResponseItem,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:识别一种特殊的“hook 提示消息”,并把它显示成客户端的 HookPrompt 条目。hook 可以理解成某些自动化步骤插入的提示。

数据流:进去的是一个原始响应条目 → 它只接受角色为 user 的消息,再尝试解析里面是否是 hook prompt → 如果是,就转成 HookPrompt 并发 ItemCompleted;如果不是就什么也不做。

调用关系:apply_bespoke_event_handling 处理 RawResponseItem 时先调用它;测试 test_hook_prompt_raw_response_emits_item_completed 验证能从原始消息里识别 hook 提示。

调用图:调用 3 个内部函数(now_unix_timestamp_ms, send_server_notification, parse_hook_prompt_message);被 2 处调用(apply_bespoke_event_handling, test_hook_prompt_raw_response_emits_item_completed);外部调用 2 个(ItemCompleted, to_string)。

find_and_remove_turn_summary1495–1501 ↗
async fn find_and_remove_turn_summary(
    _conversation_id: ThreadId,
    thread_state: &Arc<Mutex<ThreadState>>,
) -> TurnSummary

作用:取出并清空当前轮的摘要状态。摘要里记录了本轮开始时间、最后错误、已开始的命令等临时信息。

数据流:进去的是线程状态 → 它加锁取走 turn_summary,并把原位置恢复成空的默认摘要 → 返回刚才取出的摘要。

调用关系:handle_turn_complete 和 handle_turn_interrupted 在一轮结束时调用它,表示这轮的临时账本结清;测试 test_handle_error_records_message 也用它检查错误是否被记住。

调用图:被 3 处调用(handle_turn_complete, handle_turn_interrupted, test_handle_error_records_message);外部调用 1 个(take)。

handle_turn_complete1503–1530 ↗
async fn handle_turn_complete(
    conversation_id: ThreadId,
    event_turn_id: String,
    turn_complete_event: TurnCompleteEvent,
    outgoing: &ThreadScopedOutgoingMessageSender,
    thread_state:

作用:处理正常结束的一轮对话。它会判断这一轮是否曾记录过错误,从而决定最终状态是 Completed 还是 Failed。

数据流:进去的是线程编号、轮次编号、核心的完成事件和线程状态 → 它取出本轮摘要,看 last_error 是否存在 → 发出带完成时间、耗时和状态的 TurnCompleted 通知。

调用关系:apply_bespoke_event_handling 收到 TurnComplete 时调用它;它依赖 find_and_remove_turn_summary 和 emit_turn_completed_with_status;多个测试验证无错、有错、多轮情况下的结果。

调用图:调用 2 个内部函数(emit_turn_completed_with_status, find_and_remove_turn_summary);被 4 处调用(apply_bespoke_event_handling, test_handle_turn_complete_emits_completed_without_error, test_handle_turn_complete_emits_error_multiple_turns, test_handle_turn_complete_emits_failed_with_error)。

handle_turn_interrupted1532–1554 ↗
async fn handle_turn_interrupted(
    conversation_id: ThreadId,
    event_turn_id: String,
    turn_aborted_event: TurnAbortedEvent,
    outgoing: &ThreadScopedOutgoingMessageSender,
    thread_state

作用:处理被用户或系统打断的一轮对话。它把这一轮标成 Interrupted,而不是成功或失败。

数据流:进去的是线程编号、轮次编号、中断事件和线程状态 → 它取出并清空本轮摘要 → 发出状态为 Interrupted 的 TurnCompleted 通知,错误字段为空。

调用关系:apply_bespoke_event_handling 收到 TurnAborted 时调用它;它和 handle_turn_complete 共用 find_and_remove_turn_summary、emit_turn_completed_with_status。

调用图:调用 2 个内部函数(emit_turn_completed_with_status, find_and_remove_turn_summary);被 2 处调用(apply_bespoke_event_handling, test_handle_turn_interrupted_emits_interrupted_with_error)。

handle_thread_rollback_failed1556–1569 ↗
async fn handle_thread_rollback_failed(
    _conversation_id: ThreadId,
    message: String,
    thread_state: &Arc<Mutex<ThreadState>>,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:处理线程回滚失败。回滚就是把对话恢复到旧状态;失败时要回复正在等待的回滚请求,避免客户端一直等。

数据流:进去的是错误消息、线程状态和发送器 → 它取出 pending_rollbacks 里等待回复的请求编号 → 如果有,就用 invalid_request 错误回复客户端。

调用关系:apply_bespoke_event_handling 收到特定 ThreadRollbackFailed 错误时调用它,并且不会再额外发普通错误通知。

调用图:调用 2 个内部函数(invalid_request, send_error);被 1 处调用(apply_bespoke_event_handling)。

thread_rollback_response_from_stored_thread1571–1590 ↗
fn thread_rollback_response_from_stored_thread(
    stored_thread: codex_thread_store::StoredThread,
    session_id: String,
    fallback_model_provider: &str,
    fallback_cwd: &AbsolutePathBuf,

作用:把磁盘里读出的旧线程记录重新拼成客户端需要的回滚响应。它解决“回滚后前端要重新拿到完整线程视图”的问题。

数据流:进去的是存储线程、会话编号、兜底模型提供方、兜底工作目录和加载状态 → 它先转成客户端线程对象,再用历史记录填充轮次和条目 → 成功时返回 ThreadRollbackResponse,缺少历史时返回错误文字。

调用关系:apply_bespoke_event_handling 在收到 ThreadRolledBack 后用它生成请求回复;测试 rollback_response_rebuilds_pathless_thread_from_stored_history 验证路径缺失时也能从历史恢复。

调用图:被 2 处调用(apply_bespoke_event_handling, rollback_response_rebuilds_pathless_thread_from_stored_history);外部调用 3 个(populate_thread_turns_from_history, thread_from_stored_thread, format!)。

respond_to_pending_interrupts1592–1606 ↗
async fn respond_to_pending_interrupts(
    thread_state: &Arc<Mutex<ThreadState>>,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:回复所有还在等待的“打断请求”。当一轮已经完成或中断时,打断请求就应该收尾。

数据流:进去的是线程状态和发送器 → 它取出并清空 pending_interrupts 列表 → 对每个请求编号发送 TurnInterruptResponse。

调用关系:apply_bespoke_event_handling 在 TurnComplete 和 TurnAborted 时调用它,确保客户端发出的 interrupt 不会悬空。

调用图:调用 1 个内部函数(send_response);被 1 处调用(apply_bespoke_event_handling);外部调用 1 个(take)。

handle_token_count_event1608–1634 ↗
async fn handle_token_count_event(
    conversation_id: ThreadId,
    turn_id: String,
    token_count_event: TokenCountEvent,
    outgoing: &ThreadScopedOutgoingMessageSender,
)

作用:把 token 用量和账号限流信息通知客户端。token 可以理解成模型计费和上下文长度使用的基本单位。

数据流:进去的是线程编号、轮次编号和 TokenCountEvent → 如果有用量信息,就发 ThreadTokenUsageUpdated;如果有限流信息,就发 AccountRateLimitsUpdated → 没有对应信息就不发。

调用关系:apply_bespoke_event_handling 收到 TokenCount 时调用它;测试覆盖有用量、有额度信息和完全没有信息两种情况。

调用图:调用 1 个内部函数(send_server_notification);被 3 处调用(apply_bespoke_event_handling, test_handle_token_count_event_emits_usage_and_rate_limits, test_handle_token_count_event_without_usage_info);外部调用 3 个(AccountRateLimitsUpdated, ThreadTokenUsageUpdated, to_string)。

handle_error1636–1643 ↗
async fn handle_error(
    _conversation_id: ThreadId,
    error: TurnError,
    thread_state: &Arc<Mutex<ThreadState>>,
)

作用:把本轮发生的错误记到账本里。它本身不通知客户端,只负责保存状态,供轮次结束时判断失败。

数据流:进去的是错误和线程状态 → 它加锁修改 turn_summary.last_error → 没有返回值,只改变内存里的线程摘要。

调用关系:handle_error_notification 会先调用它再发错误通知;多个测试直接调用它来模拟一轮中出现过错误。

调用图:被 5 处调用(handle_error_notification, test_handle_error_records_message, test_handle_turn_complete_emits_error_multiple_turns, test_handle_turn_complete_emits_failed_with_error, test_handle_turn_interrupted_emits_interrupted_with_error)。

handle_error_notification1645–1661 ↗
async fn handle_error_notification(
    conversation_id: ThreadId,
    event_turn_id: &str,
    error: TurnError,
    outgoing: &ThreadScopedOutgoingMessageSender,
    thread_state: &Arc<Mutex<ThreadS

作用:记录错误并马上通知客户端。适合那些会影响本轮最终状态的错误。

数据流:进去的是线程编号、轮次编号、错误内容、发送器和线程状态 → 它先调用 handle_error 保存错误 → 再发 Error 通知,will_retry 标成 false。

调用关系:apply_bespoke_event_handling 处理普通 Error 时调用它;on_request_permissions_response 遇到权限路径转换失败时也用它报告问题。

调用图:调用 2 个内部函数(handle_error, send_server_notification);被 2 处调用(apply_bespoke_event_handling, on_request_permissions_response);外部调用 3 个(Error, clone, to_string)。

on_request_user_input_response1663–1742 ↗
async fn on_request_user_input_response(
    event_turn_id: String,
    pending_request_id: RequestId,
    receiver: oneshot::Receiver<ClientRequestResult>,
    conversation: Arc<CodexThread>,
    thr

作用:等待客户端回答工具提出的问题,然后把答案交回核心引擎。比如工具问用户“请选择哪个选项”。

数据流:进去的是轮次编号、请求编号、一次性接收器、核心线程、线程状态和活动标记 → 它等待客户端结果,清理挂起请求;失败时提交空答案,成功时把 JSON 转成核心能懂的答案 → 最后提交 Op::UserInputAnswer。

调用关系:apply_bespoke_event_handling 发出 ToolRequestUserInput 请求后会启动异步任务调用它;它把客户端的回答接回核心线程。

调用图:调用 2 个内部函数(is_turn_transition_server_request_error, resolve_server_request_on_thread_listener);被 1 处调用(apply_bespoke_event_handling);外部调用 2 个(new, error!)。

on_mcp_server_elicitation_response1744–1770 ↗
async fn on_mcp_server_elicitation_response(
    server_name: String,
    request_id: codex_protocol::mcp::RequestId,
    pending_request_id: RequestId,
    receiver: oneshot::Receiver<ClientRequestRe

作用:等待客户端回答 MCP 服务器的询问,并把选择交回核心。MCP 是外部工具/服务接入协议,这里的 elicitation 是服务向用户索要信息。

数据流:进去的是服务器名、MCP 请求编号、app-server 请求编号、接收器、核心线程和线程状态 → 它等客户端回复,清理挂起状态,再把结果转成核心动作 → 提交 Op::ResolveElicitation。

调用关系:apply_bespoke_event_handling 发送 McpServerElicitationRequest 后启动它;它依赖 mcp_server_elicitation_response_from_client_result 做容错转换。

调用图:调用 2 个内部函数(mcp_server_elicitation_response_from_client_result, resolve_server_request_on_thread_listener);被 1 处调用(apply_bespoke_event_handling);外部调用 1 个(error!)。

mcp_server_elicitation_response_from_client_result1772–1809 ↗
fn mcp_server_elicitation_response_from_client_result(
    response: std::result::Result<ClientRequestResult, oneshot::error::RecvError>,
) -> McpServerElicitationRequestResponse

作用:把客户端对 MCP 询问的回复变成一个安全的默认结果。即使客户端断线或回错格式,也不会让流程崩掉。

数据流:进去的是一次性通道收到的结果或错误 → 成功时解析成 McpServerElicitationRequestResponse;轮次切换错误转成 Cancel;其他失败转成 Decline → 返回一个明确动作。

调用关系:on_mcp_server_elicitation_response 调用它;测试 mcp_server_elicitation_turn_transition_error_maps_to_cancel 验证轮次切换时会取消而不是拒绝。

调用图:调用 1 个内部函数(is_turn_transition_server_request_error);被 2 处调用(on_mcp_server_elicitation_response, mcp_server_elicitation_turn_transition_error_maps_to_cancel);外部调用 1 个(error!)。

on_request_permissions_response1811–1870 ↗
async fn on_request_permissions_response(
    pending_response: PendingRequestPermissionsResponse,
    conversation: Arc<CodexThread>,
    thread_state: Arc<Mutex<ThreadState>>,
)

作用:等待客户端对权限申请的审批,并把最终允许的权限交给核心。权限包括文件读写、网络访问等。

数据流:进去的是一包挂起权限请求信息、核心线程和线程状态 → 它等待客户端回复,清理挂起请求,转换并裁剪权限;路径转换失败会发错误并打断当前轮 → 成功时记录审批结果并提交 Op::RequestPermissionsResponse。

调用关系:apply_bespoke_event_handling 处理 RequestPermissions 后启动它;它主要依赖 request_permissions_response_from_client_result 做权限转换,必要时调用 handle_error_notification。

调用图:调用 3 个内部函数(handle_error_notification, request_permissions_response_from_client_result, resolve_server_request_on_thread_listener);被 1 处调用(apply_bespoke_event_handling);外部调用 2 个(error!, format!)。

request_permissions_response_from_client_result1884–1944 ↗
fn request_permissions_response_from_client_result(
    requested_permissions: CoreRequestPermissionProfile,
    response: std::result::Result<ClientRequestResult, oneshot::error::RecvError>,
    cwd:

作用:把客户端批准的权限转成核心真正可执行的权限,并且不会允许超出原申请范围。它是权限安全边界之一。

数据流:进去的是核心原本申请的权限、客户端回复结果和当时的工作目录 → 它处理断线、格式错误和轮次切换;成功时解析客户端授予的权限,并与原申请求交集 → 返回核心权限响应,或在轮次切换时返回 None。

调用关系:on_request_permissions_response 调用它;大量测试覆盖部分授权、会话级授权、严格自动审查、路径越界和更宽权限被拒绝等情况。

调用图:调用 2 个内部函数(is_turn_transition_server_request_error, intersect_permission_profiles);被 9 处调用(on_request_permissions_response, request_permissions_response_accepts_explicit_child_grant_for_requested_cwd_scope, request_permissions_response_accepts_partial_network_and_file_system_grants, request_permissions_response_ignores_broader_cwd_grant_for_requested_child_path, request_permissions_response_preserves_session_scope, request_permissions_response_preserves_turn_scoped_strict_auto_review, request_permissions_response_rejects_child_grant_outside_requested_cwd_scope, request_permissions_response_rejects_session_scoped_strict_auto_review, request_permissions_turn_transition_error_is_ignored);外部调用 5 个(default, into, default, error!, matches!)。

render_review_output_text1948–1966 ↗
fn render_review_output_text(output: &ReviewOutputEvent) -> String

作用:把代码审查结果整理成一段适合显示给用户的文字。没有有效内容时给出兜底提示。

数据流:进去的是审查输出,里面可能有总体说明和问题列表 → 它去掉空白,把说明和格式化后的问题块拼在一起 → 返回最终显示文本;如果都为空,返回固定失败提示。

调用关系:apply_bespoke_event_handling 在 ExitedReviewMode 时调用它,把 ReviewOutputEvent 转成 ThreadItem::ExitedReviewMode 的文字。

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

map_file_change_approval_decision1968–1975 ↗
fn map_file_change_approval_decision(decision: FileChangeApprovalDecision) -> ReviewDecision

作用:把客户端对文件改动的选择翻译成核心审查决定。两个系统使用的枚举名字不同,需要这层转换。

数据流:进去的是 FileChangeApprovalDecision,比如接受、整会话接受、拒绝、取消 → 它逐项映射成 ReviewDecision → 返回给调用者。

调用关系:on_file_change_request_approval_response 用它处理客户端审批结果;测试 file_change_accept_for_session_maps_to_approved_for_session 检查关键映射。

调用图:被 2 处调用(on_file_change_request_approval_response, file_change_accept_for_session_maps_to_approved_for_session)。

on_file_change_request_approval_response1978–2021 ↗
async fn on_file_change_request_approval_response(
    item_id: String,
    pending_request_id: RequestId,
    receiver: oneshot::Receiver<ClientRequestResult>,
    codex: Arc<CodexThread>,
    thread

作用:等待客户端批准或拒绝文件改动,然后把决定告诉核心。比如模型想 apply patch 改文件时会走这里。

数据流:进去的是条目编号、请求编号、接收器、核心线程、线程状态和权限活动标记 → 它等待客户端回复,清理挂起请求;成功时解析决定,失败时默认拒绝 → 提交 Op::PatchApproval。

调用关系:apply_bespoke_event_handling 处理 ApplyPatchApprovalRequest 后启动它;它使用 map_file_change_approval_decision 完成协议转换。

调用图:调用 3 个内部函数(map_file_change_approval_decision, is_turn_transition_server_request_error, resolve_server_request_on_thread_listener);被 1 处调用(apply_bespoke_event_handling);外部调用 1 个(error!)。

on_command_execution_request_approval_response2024–2146 ↗
async fn on_command_execution_request_approval_response(
    event_turn_id: String,
    conversation_id: ThreadId,
    approval_id: Option<String>,
    item_id: String,
    completion_item: Option<Com

作用:等待客户端批准或拒绝命令执行,并把决定交回核心。它还负责在命令被拒绝时给前端补一个“命令结束”的显示。

数据流:进去的是轮次、线程、审批编号、命令条目信息、请求编号、接收器、核心线程、发送器和状态 → 它等待回复,清理挂起请求;把客户端决定转成 ReviewDecision;必要时调用 complete_command_execution_item 收尾 → 最后提交 Op::ExecApproval。

调用关系:apply_bespoke_event_handling 在 ExecApprovalRequest 后启动它;它和 start_command_execution_item/complete_command_execution_item 配合,保证命令审批的显示生命周期完整。

调用图:调用 3 个内部函数(complete_command_execution_item, is_turn_transition_server_request_error, resolve_server_request_on_thread_listener);被 1 处调用(apply_bespoke_event_handling);外部调用 1 个(error!)。

now_unix_timestamp_ms2148–2153 ↗
fn now_unix_timestamp_ms() -> i64

作用:取当前 Unix 时间戳,单位是毫秒。Unix 时间戳就是从 1970 年 1 月 1 日到现在经过的时间。

数据流:进去没有业务输入 → 它读取系统时间,换算成毫秒整数;如果系统时间异常就返回 0 → 输出 i64 时间戳。

调用关系:apply_bespoke_event_handling、start_command_execution_item、complete_command_execution_item、maybe_emit_hook_prompt_item_completed 用它给通知打时间。

调用图:被 4 处调用(apply_bespoke_event_handling, complete_command_execution_item, maybe_emit_hook_prompt_item_completed, start_command_execution_item);外部调用 1 个(now)。

tests::new_thread_state2210–2212 ↗
fn new_thread_state() -> Arc<Mutex<ThreadState>>

作用:给测试创建一份新的线程状态。这样每个测试都有干净的临时账本。

数据流:进去没有输入 → 它创建默认 ThreadState,包进异步互斥锁,再包进 Arc 共享指针 → 返回可在异步测试里共享的状态。

调用关系:许多测试都用它准备环境,比如命令开始/结束、错误记录、轮次完成等测试。

调用图:外部调用 3 个(new, new, default)。

tests::recv_broadcast_message2217–2228 ↗
async fn recv_broadcast_message(
        rx: &mut mpsc::Receiver<OutgoingEnvelope>,
    ) -> Result<OutgoingMessage>

作用:测试里从消息通道收一条发出的消息,并统一拆开外壳。它让断言更简单。

数据流:进去的是一个异步接收端 → 它等待下一条 OutgoingEnvelope;无消息就报错 → 返回里面真正的 OutgoingMessage。

调用关系:大量通知类测试调用它读取被测函数发出的消息,然后检查消息内容是否正确。

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

tests::rollback_response_rebuilds_pathless_thread_from_stored_history2231–2299 ↗
fn rollback_response_rebuilds_pathless_thread_from_stored_history() -> Result<()>

作用:测试回滚响应能从存储历史里重建线程,即使线程本身没有 rollout 路径。这样回滚后客户端仍能看到对话内容。

数据流:进去没有外部输入 → 它构造一个带历史的 StoredThread,调用 thread_rollback_response_from_stored_thread → 断言返回线程的编号、状态、预览和条目数量都正确。

调用关系:它直接覆盖 thread_rollback_response_from_stored_thread 的关键场景。

调用图:调用 3 个内部函数(thread_rollback_response_from_stored_thread, read_only, from_string);外部调用 4 个(now, assert_eq!, test_path_buf, vec!)。

tests::turn_complete_event2301–2309 ↗
fn turn_complete_event(turn_id: &str) -> TurnCompleteEvent

作用:测试辅助函数,快速造一个固定时间和耗时的 TurnCompleteEvent。

数据流:进去的是轮次编号字符串 → 它填入固定 completed_at 和 duration_ms → 返回核心完成事件。

调用关系:多个轮次完成相关测试用它,避免每个测试重复写同样的事件字段。

tests::turn_aborted_event2311–2318 ↗
fn turn_aborted_event(turn_id: &str) -> TurnAbortedEvent

作用:测试辅助函数,快速造一个固定时间和耗时的 TurnAbortedEvent。

数据流:进去的是轮次编号字符串 → 它填入中断原因、完成时间和耗时 → 返回核心中断事件。

调用关系:test_handle_turn_interrupted_emits_interrupted_with_error 用它模拟一轮被打断。

tests::command_execution_completion_item2320–2328 ↗
fn command_execution_completion_item(command: &str) -> CommandExecutionCompletionItem

作用:测试辅助函数,创建一个命令执行显示项需要的基本资料。

数据流:进去的是命令文本 → 它设置测试工作目录和一个 Unknown 命令动作 → 返回 CommandExecutionCompletionItem。

调用关系:命令开始和命令完成相关测试用它准备输入。

调用图:外部调用 2 个(test_path_buf, vec!)。

tests::guardian_command_assessment2330–2376 ↗
fn guardian_command_assessment(
        id: &str,
        turn_id: &str,
        status: GuardianAssessmentStatus,
    ) -> GuardianAssessmentEvent

作用:测试辅助函数,构造 Guardian 安全审查事件。Guardian 可以理解成自动安全检查员。

数据流:进去的是目标命令编号、轮次编号和审查状态 → 它按状态填风险、授权、理由和命令动作 → 返回 GuardianAssessmentEvent。

调用关系:guardian_command_execution_notifications_wrap_review_lifecycle 用它模拟安全审查从进行中到批准或拒绝的过程。

调用图:外部调用 4 个(format!, json!, matches!, from_value)。

tests::GuardianAssessmentTestContext::apply_guardian_assessment_event2388–2405 ↗
async fn apply_guardian_assessment_event(&self, assessment: GuardianAssessmentEvent)

作用:测试上下文里的便捷方法,用来把一个 Guardian 审查事件送进真实事件处理入口。

数据流:进去的是 GuardianAssessmentEvent → 它包装成 Event,并带上测试环境里的线程、发送器、状态和 watch manager → 调用 apply_bespoke_event_handling。

调用关系:guardian_command_execution_notifications_wrap_review_lifecycle 用它多次驱动同一个测试场景。

调用图:调用 1 个内部函数(apply_bespoke_event_handling);外部调用 5 个(new, clone, clone, GuardianAssessment, new)。

tests::guardian_assessment_started_uses_event_turn_id_fallback2409–2452 ↗
fn guardian_assessment_started_uses_event_turn_id_fallback()

作用:测试 Guardian 审查事件自己没有 turn_id 时,会退回使用外层事件的轮次编号。

数据流:进去没有外部输入 → 它构造 turn_id 为空的审查事件,调用 guardian_auto_approval_review_notification → 断言通知里的 turn_id 是外层传入值。

调用关系:它验证 apply_bespoke_event_handling 依赖的通知构造行为,避免前端收到空轮次。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_eq!, guardian_auto_approval_review_notification, test_path_buf, panic!)。

tests::guardian_assessment_completed_emits_review_payload2455–2505 ↗
fn guardian_assessment_completed_emits_review_payload()

作用:测试 Guardian 审查完成时通知内容完整,包括状态、风险、理由和动作。

数据流:进去没有外部输入 → 它构造一个被拒绝的命令审查事件 → 调用通知构造函数并断言各字段。

调用关系:它保护 Guardian 完成通知的协议格式。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, guardian_auto_approval_review_notification, test_path_buf, panic!)。

tests::guardian_assessment_aborted_emits_completed_review_payload2508–2551 ↗
fn guardian_assessment_aborted_emits_completed_review_payload()

作用:测试 Guardian 审查被中止时,也会发“审查完成”类通知,而不是消失不见。

数据流:进去没有外部输入 → 它构造一个网络访问审查被中止的事件 → 断言通知状态是 Aborted,目标项可为空。

调用关系:它覆盖安全审查的异常结束分支。

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

tests::command_execution_started_helper_emits_once2554–2622 ↗
async fn command_execution_started_helper_emits_once() -> Result<()>

作用:测试 start_command_execution_item 对同一个命令只发一次开始通知。

数据流:进去没有外部输入 → 它建立消息通道和线程状态,第一次调用 helper 后读取通知;第二次用同一命令编号调用 → 断言第二次没有新消息。

调用关系:它直接验证 start_command_execution_item 的防重复机制。

调用图:调用 5 个内部函数(disabled, start_command_execution_item, new, new, new);外部调用 9 个(new, command_execution_completion_item, new_thread_state, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

tests::complete_command_execution_item_emits_declined_once_for_pending_command2625–2701 ↗
async fn complete_command_execution_item_emits_declined_once_for_pending_command() -> Result<()>

作用:测试命令完成通知只会对已经开始的命令发一次。

数据流:进去没有外部输入 → 它先启动命令项,再完成为 Declined 并读取完成通知;之后再次完成同一命令 → 断言没有重复通知。

调用关系:它覆盖 start_command_execution_item 与 complete_command_execution_item 之间的状态配合。

调用图:调用 6 个内部函数(disabled, complete_command_execution_item, start_command_execution_item, new, new, new);外部调用 9 个(new, command_execution_completion_item, new_thread_state, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

tests::guardian_command_execution_notifications_wrap_review_lifecycle2704–2906 ↗
async fn guardian_command_execution_notifications_wrap_review_lifecycle() -> Result<()>

作用:测试 Guardian 命令安全审查如何包住命令执行显示生命周期。进行中时显示命令和审查,拒绝时命令也要结束。

数据流:进去没有外部输入 → 它启动真实测试线程,依次送入批准和拒绝场景的 Guardian 事件 → 读取并断言 ItemStarted、审查开始、审查完成、命令完成等通知顺序。

调用关系:它通过 GuardianAssessmentTestContext::apply_guardian_assessment_event 间接调用 apply_bespoke_event_handling,覆盖主入口里 GuardianAssessment 分支。

调用图:调用 7 个内部函数(disabled, new, new, new, thread_manager_with_models_provider_and_home, default_for_tests, create_dummy_chatgpt_auth_for_testing);外部调用 11 个(new, new, guardian_command_assessment, new_thread_state, recv_broadcast_message, assert!, assert_eq!, bail!, load_default_config_for_test, channel (+1 more))。

tests::file_change_accept_for_session_maps_to_approved_for_session2909–2913 ↗
fn file_change_accept_for_session_maps_to_approved_for_session()

作用:测试文件改动的“本会话接受”能正确映射成核心的“本会话批准”。

数据流:进去没有外部输入 → 它调用 map_file_change_approval_decision → 断言结果是 ApprovedForSession。

调用关系:它直接保护文件审批转换函数的一个重要分支。

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

tests::mcp_server_elicitation_turn_transition_error_maps_to_cancel2916–2933 ↗
fn mcp_server_elicitation_turn_transition_error_maps_to_cancel()

作用:测试 MCP 询问在轮次切换导致请求失效时,会被转成 Cancel。

数据流:进去没有外部输入 → 它构造一个带 turnTransition 原因的客户端错误 → 调用 mcp_server_elicitation_response_from_client_result → 断言动作是 Cancel。

调用关系:它覆盖 MCP 回复转换的特殊容错路径。

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

tests::request_permissions_turn_transition_error_is_ignored2936–2951 ↗
fn request_permissions_turn_transition_error_is_ignored()

作用:测试权限请求如果因为轮次切换而失效,会被忽略而不是默认拒绝。

数据流:进去没有外部输入 → 它构造 turnTransition 错误并调用 request_permissions_response_from_client_result → 断言返回 None。

调用关系:它覆盖权限回复转换里“不要再回核心”的分支。

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

tests::request_permissions_response_accepts_partial_network_and_file_system_grants2954–3055 ↗
fn request_permissions_response_accepts_partial_network_and_file_system_grants()

作用:测试客户端可以只批准申请权限的一部分。系统应只把交集交给核心。

数据流:进去没有外部输入 → 它准备网络和文件系统权限申请,再用多组客户端授权 JSON 调用转换函数 → 断言结果只包含被申请且被批准的部分。

调用关系:它重点验证 request_permissions_response_from_client_result 和 intersect_permission_profiles 的安全裁剪效果。

调用图:调用 1 个内部函数(request_permissions_response_from_client_result);外部调用 6 个(from_read_write_roots, assert_eq!, cfg!, json!, current_dir, vec!)。

tests::request_permissions_response_preserves_session_scope3058–3078 ↗
fn request_permissions_response_preserves_session_scope()

作用:测试客户端批准的会话级权限范围会被保留下来。会话级表示这次会话后续也有效。

数据流:进去没有外部输入 → 它构造 scope 为 session 的权限回复 → 调用转换函数 → 断言核心响应 scope 也是 Session。

调用关系:它覆盖权限转换的授权范围字段。

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

tests::request_permissions_response_rejects_session_scoped_strict_auto_review3081–3106 ↗
fn request_permissions_response_rejects_session_scoped_strict_auto_review()

作用:测试严格自动审查不能和会话级授权一起使用。这样避免一次性给出过宽的自动批准。

数据流:进去没有外部输入 → 它构造 session scope 且 strictAutoReview 为 true 的回复 → 调用转换函数 → 断言结果退回默认无权限、Turn 范围、strict 为 false。

调用关系:它保护 request_permissions_response_from_client_result 里的安全限制。

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

tests::request_permissions_response_preserves_turn_scoped_strict_auto_review3109–3132 ↗
fn request_permissions_response_preserves_turn_scoped_strict_auto_review()

作用:测试严格自动审查在单轮授权里是允许保留的。

数据流:进去没有外部输入 → 它构造 turn 范围的网络权限授权并带 strictAutoReview → 调用转换函数 → 断言 strict_auto_review 为 true。

调用关系:它和拒绝 session scoped strict auto review 的测试互相补充。

调用图:调用 1 个内部函数(request_permissions_response_from_client_result);外部调用 5 个(default, assert!, assert_eq!, json!, current_dir)。

tests::request_permissions_response_accepts_explicit_child_grant_for_requested_cwd_scope3135–3176 ↗
fn request_permissions_response_accepts_explicit_child_grant_for_requested_cwd_scope()

作用:测试当申请的是当前项目范围写权限时,客户端批准其中一个子目录是可以接受的。

数据流:进去没有外部输入 → 它创建临时目录作为 cwd,申请项目根写权限,客户端只批准 child 子目录 → 转换后断言核心只拿到 child 写权限。

调用关系:它覆盖权限路径从特殊项目根到实际子路径的安全交集逻辑。

调用图:调用 2 个内部函数(request_permissions_response_from_client_result, from_absolute_path);外部调用 5 个(default, new, assert_eq!, json!, vec!)。

tests::request_permissions_response_rejects_child_grant_outside_requested_cwd_scope3179–3217 ↗
fn request_permissions_response_rejects_child_grant_outside_requested_cwd_scope()

作用:测试客户端不能把授权偷偷指向另一个工作目录下的子路径。

数据流:进去没有外部输入 → 它申请 request-cwd 的项目范围权限,却让客户端批准 later-cwd/child → 转换后断言没有任何权限。

调用关系:它保护 request_permissions_response_from_client_result 的路径边界检查。

调用图:调用 2 个内部函数(request_permissions_response_from_client_result, from_absolute_path);外部调用 5 个(default, new, assert_eq!, json!, vec!)。

tests::request_permissions_response_ignores_broader_cwd_grant_for_requested_child_path3220–3259 ↗
fn request_permissions_response_ignores_broader_cwd_grant_for_requested_child_path()

作用:测试如果原本只申请某个子目录,客户端批准更宽的项目根权限也不会被接受。

数据流:进去没有外部输入 → 它申请 child 写权限,客户端返回整个项目根写权限 → 转换后断言结果为空。

调用关系:它验证权限不会被客户端回复扩大。

调用图:调用 2 个内部函数(request_permissions_response_from_client_result, from_absolute_path);外部调用 6 个(from_read_write_roots, default, new, assert_eq!, json!, vec!)。

tests::test_handle_error_records_message3262–3287 ↗
async fn test_handle_error_records_message() -> Result<()>

作用:测试 handle_error 会把错误记录进当前轮摘要。

数据流:进去没有外部输入 → 它创建线程状态,调用 handle_error 写入 boom 错误,再取出摘要 → 断言 last_error 等于写入内容。

调用关系:它直接覆盖 handle_error 和 find_and_remove_turn_summary 的配合。

调用图:调用 3 个内部函数(find_and_remove_turn_summary, handle_error, new);外部调用 2 个(new_thread_state, assert_eq!)。

tests::turn_started_omits_active_snapshot_items3290–3375 ↗
async fn turn_started_omits_active_snapshot_items() -> Result<()>

作用:测试 TurnStarted 通知不会带上已经缓存的旧条目,而是让客户端按未加载状态处理。

数据流:进去没有外部输入 → 它先在线程状态里放入一条用户消息,再送入 TurnStarted 事件 → 读取通知并断言 items 为空、items_view 是 NotLoaded。

调用关系:它通过 apply_bespoke_event_handling 覆盖 TurnStarted 分支,避免前端重复显示旧内容。

调用图:调用 8 个内部函数(disabled, apply_bespoke_event_handling, new, new, new, thread_manager_with_models_provider_and_home, default_for_tests, create_dummy_chatgpt_auth_for_testing);外部调用 15 个(new, default, new, new, new_thread_state, recv_broadcast_message, assert!, assert_eq!, bail!, load_default_config_for_test (+5 more))。

tests::interrupted_subagent_activity_removes_missing_thread_watch3378–3463 ↗
async fn interrupted_subagent_activity_removes_missing_thread_watch() -> Result<()>

作用:测试子代理线程被中断且线程已不存在时,watch 状态会被清理。

数据流:进去没有外部输入 → 它先给一个不存在的子线程登记运行状态,再送入 Interrupted 的 SubAgentActivity → 断言 watch 数量归零,并检查发出的子代理活动通知。

调用关系:它通过 apply_bespoke_event_handling 覆盖 SubAgentActivity 分支和 ThreadWatchManager 清理逻辑。

调用图:调用 10 个内部函数(disabled, apply_bespoke_event_handling, new, new, new, thread_manager_with_models_provider_and_home, default_for_tests, create_dummy_chatgpt_auth_for_testing, try_from, new);外部调用 11 个(new, new, new_thread_state, recv_broadcast_message, assert_eq!, bail!, load_default_config_for_test, channel, SubAgentActivity, new (+1 more))。

tests::test_handle_turn_complete_emits_completed_without_error3466–3523 ↗
async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()>

作用:测试没有错误时,一轮完成会发 Completed 状态。

数据流:进去没有外部输入 → 它准备带开始时间的线程状态,调用 handle_turn_complete → 读取 TurnCompleted 通知并断言状态、时间和耗时正确。

调用关系:它直接覆盖 handle_turn_complete 的正常成功路径。

调用图:调用 5 个内部函数(disabled, handle_turn_complete, new, new, new);外部调用 12 个(new, default, new_thread_state, recv_broadcast_message, turn_complete_event, assert!, assert_eq!, bail!, channel, TurnComplete (+2 more))。

tests::test_handle_turn_interrupted_emits_interrupted_with_error3526–3573 ↗
async fn test_handle_turn_interrupted_emits_interrupted_with_error() -> Result<()>

作用:测试即使之前记录过错误,被中断的轮次也显示 Interrupted,且错误字段为空。

数据流:进去没有外部输入 → 它先调用 handle_error 记录错误,再调用 handle_turn_interrupted → 断言通知状态为 Interrupted 且没有错误。

调用关系:它保护 handle_turn_interrupted 的语义:中断不是普通失败。

调用图:调用 6 个内部函数(disabled, handle_error, handle_turn_interrupted, new, new, new);外部调用 9 个(new, new_thread_state, recv_broadcast_message, turn_aborted_event, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_handle_turn_complete_emits_failed_with_error3576–3630 ↗
async fn test_handle_turn_complete_emits_failed_with_error() -> Result<()>

作用:测试有错误记录时,一轮正常结束会被标成 Failed。

数据流:进去没有外部输入 → 它先记录 bad 错误,再调用 handle_turn_complete → 断言 TurnCompleted 里状态为 Failed,并带上错误信息。

调用关系:它覆盖 handle_turn_complete 的失败路径。

调用图:调用 6 个内部函数(disabled, handle_error, handle_turn_complete, new, new, new);外部调用 9 个(new, new_thread_state, recv_broadcast_message, turn_complete_event, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_handle_turn_plan_update_emits_notification_for_v23633–3678 ↗
async fn test_handle_turn_plan_update_emits_notification_for_v2() -> Result<()>

作用:测试计划更新会发出 v2 客户端协议的 TurnPlanUpdated 通知。

数据流:进去没有外部输入 → 它构造两步计划,调用 handle_turn_plan_update → 读取通知并检查说明、步骤文字和状态。

调用关系:它直接覆盖 handle_turn_plan_update。

调用图:调用 5 个内部函数(disabled, handle_turn_plan_update, new, new, new);外部调用 7 个(new, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_handle_token_count_event_emits_usage_and_rate_limits3681–3771 ↗
async fn test_handle_token_count_event_emits_usage_and_rate_limits() -> Result<()>

作用:测试 token 用量和限流信息都会被通知给客户端。

数据流:进去没有外部输入 → 它构造 token 使用量和 rate limit 快照,调用 handle_token_count_event → 连续读取两条通知并检查字段。

调用关系:它覆盖 handle_token_count_event 同时发两类通知的情况。

调用图:调用 5 个内部函数(disabled, handle_token_count_event, new, new, new);外部调用 7 个(new, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_handle_token_count_event_without_usage_info3774–3804 ↗
async fn test_handle_token_count_event_without_usage_info() -> Result<()>

作用:测试没有 token 用量和限流信息时,不应发任何通知。

数据流:进去没有外部输入 → 它传入 info 和 rate_limits 都为空的 TokenCountEvent → 调用处理函数后断言通道里没有消息。

调用关系:它覆盖 handle_token_count_event 的空输入分支。

调用图:调用 5 个内部函数(disabled, handle_token_count_event, new, new, new);外部调用 4 个(new, assert!, channel, vec!)。

tests::test_handle_turn_complete_emits_error_multiple_turns3807–3926 ↗
async fn test_handle_turn_complete_emits_error_multiple_turns() -> Result<()>

作用:测试错误摘要会在一轮结束后清空,不会污染后续轮次。

数据流:进去没有外部输入 → 它模拟多个线程/轮次依次记录错误和完成 → 读取三条完成通知,断言前两轮失败、后一轮成功。

调用关系:它验证 handle_error、handle_turn_complete、find_and_remove_turn_summary 的状态清理效果。

调用图:调用 6 个内部函数(disabled, handle_error, handle_turn_complete, new, new, new);外部调用 9 个(new, new_thread_state, recv_broadcast_message, turn_complete_event, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_handle_turn_diff_emits_v2_notification3929–3966 ↗
async fn test_handle_turn_diff_emits_v2_notification() -> Result<()>

作用:测试代码差异更新会发出正确的 v2 通知。

数据流:进去没有外部输入 → 它构造一段 unified diff,调用 handle_turn_diff → 读取通知并检查线程、轮次和 diff 文本。

调用关系:它直接覆盖 handle_turn_diff。

调用图:调用 5 个内部函数(disabled, handle_turn_diff, new, new, new);外部调用 7 个(new, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

tests::test_hook_prompt_raw_response_emits_item_completed3969–4017 ↗
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()>

作用:测试原始响应里的 hook prompt 能被识别并显示成完成的 HookPrompt 条目。

数据流:进去没有外部输入 → 它构造包含两个 hook 片段的原始消息,调用 maybe_emit_hook_prompt_item_completed → 读取 ItemCompleted 通知并检查片段内容。

调用关系:它直接覆盖 maybe_emit_hook_prompt_item_completed 的成功解析路径。

调用图:调用 6 个内部函数(disabled, maybe_emit_hook_prompt_item_completed, new, new, build_hook_prompt_message, new);外部调用 8 个(new, from_single_hook, recv_broadcast_message, assert!, assert_eq!, bail!, channel, vec!)。

app-server/src/thread_status.rs源码 ↗
domain_logiccross-cutting:线程加载、运行、等待输入、结束、报错和卸载时都会用到

可以把这个文件想成一个调度室里的白板。每个线程是一辆车,白板上写着它是停着、开着、等放行,还是坏了。ThreadWatchManager 是对外入口,别的地方告诉它“某线程开始运行了”“要等用户批准”“线程关掉了”。它把这些零散事件合成 ThreadStatus,并在状态真的变了时通知外部。它还用 watch 通道(一种只保留最新值、订阅者能收到变化的通知管道)让别人订阅单个线程或正在运行的线程数量。ThreadWatchActiveGuard 是一个很重要的小保险:当代码请求权限或用户输入时会拿到它,等这个 guard 被丢弃,就自动把“等待中”的计数减掉,防止状态永远卡在等待。

函数细节52
ThreadWatchActiveGuard::new34–45 ↗
fn new(
        manager: ThreadWatchManager,
        thread_id: String,
        guard_type: ThreadWatchActiveGuardType,
    ) -> Self

作用:创建一个“活动守卫”。它代表某个线程当前有一件还没结束的等待事项,比如等批准或等用户输入。

数据流:进去的是管理器、线程编号和等待类型 → 它把这些保存下来,并记住当前异步运行环境的句柄 → 出来一个 guard,之后只要这个 guard 还活着,等待事项就被认为还没结束。

调用关系:它由 ThreadWatchManager::note_pending_request 创建。后续真正的收尾不是调用者手动做,而是在 ThreadWatchActiveGuard::drop 里自动交回给管理器。

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

ThreadWatchActiveGuard::drop49–58 ↗
fn drop(&mut self)

作用:当等待事项结束、guard 被释放时,自动通知管理器把对应的等待计数减一。

数据流:进去的是 guard 里保存的管理器、线程编号和等待类型 → 它复制这些信息,在线程运行时里启动一个异步任务 → 结果是管理器稍后更新状态,可能把线程从“等批准/等输入”改回普通活动状态。

调用关系:这是 Rust 的自动清理钩子,不需要业务代码显式调用。它把收尾工作交给 ThreadWatchManager::note_active_guard_released,避免调用者忘记清理。

调用图:外部调用 2 个(spawn, clone)。

ThreadWatchManager::default68–70 ↗
fn default() -> Self

作用:提供默认创建方式,让需要默认值的地方能直接得到一个空的线程状态管理器。

数据流:没有额外输入 → 它调用 ThreadWatchManager::new → 出来一个没有外发通知通道、内部状态为空的管理器。

调用关系:它只是 ThreadWatchManager::new 的便捷入口,适合测试或普通初始化时使用。

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

ThreadWatchManager::new74–81 ↗
fn new() -> Self

作用:新建一个线程状态管理器,但不连接对外消息发送器。适合只在内部查询或测试状态时用。

数据流:没有输入 → 它建一份空状态、一把异步互斥锁(一把锁,防止多个任务同时改同一份数据)和一个运行中线程数量的 watch 通道 → 出来一个可共享、可并发使用的管理器。

调用关系:很多测试和运行流程会先用它建立状态中心。之后 upsert、note_turn_started 等函数都围绕这个管理器工作。

调用图:被 11 处调用(guardian_command_execution_notifications_wrap_review_lifecycle, interrupted_subagent_activity_removes_missing_thread_watch, turn_started_omits_active_snapshot_items, has_running_turns_tracks_runtime_running_flag_only, loaded_status_defaults_to_not_loaded_for_untracked_threads, loaded_statuses_default_to_not_loaded_for_untracked_threads, shutdown_marks_thread_not_loaded, status_updates_track_single_thread, status_watchers_receive_only_their_thread_updates, system_error_sets_idle_flag_until_next_turn (+1 more));外部调用 4 个(new, new, default, channel)。

ThreadWatchManager::new_with_outgoing83–90 ↗
fn new_with_outgoing(outgoing: Arc<OutgoingMessageSender>) -> Self

作用:新建一个会向外发送状态变化通知的线程状态管理器。前端或客户端需要实时知道线程状态时会用它。

数据流:进去一个 OutgoingMessageSender(对外发消息的发送器)→ 它保存发送器,并初始化空状态和运行数量通道 → 出来一个既能记状态、又能广播变化的管理器。

调用关系:服务器启动或相关组件创建时会用它。之后 ThreadWatchManager::mutate_and_publish 会通过这个发送器发 ThreadStatusChanged 通知。

调用图:被 3 处调用(new, silent_upsert_skips_initial_notification, status_change_emits_notification);外部调用 4 个(new, new, default, channel)。

ThreadWatchManager::upsert_thread92–97 ↗
async fn upsert_thread(&self, thread: Thread)

作用:登记或刷新一个线程,并且在状态变化时发通知。名字里的 upsert 表示“有就更新,没有就插入”。

数据流:进去一个 Thread,其中最重要的是线程编号 → 它把该线程标记为已加载,并计算状态是否变化 → 出来没有直接返回值,但内部状态会更新,必要时会广播状态变化。

调用关系:线程恢复或监听器接上线程时会调用它。它把实际改状态和通知的活交给 ThreadWatchManager::mutate_and_publish。

调用图:调用 1 个内部函数(mutate_and_publish);被 2 处调用(thread_resume_inner, try_attach_thread_listener)。

ThreadWatchManager::upsert_thread_silently99–104 ↗
async fn upsert_thread_silently(&self, thread: Thread)

作用:静默登记或刷新一个线程,也就是更新内部状态但不发第一次状态通知。

数据流:进去一个 Thread → 它把线程标记为已加载 → 出来没有直接返回值,状态会变成已知状态,但初始通知被刻意跳过。

调用关系:分叉线程或后台启动某些流程时会用它,避免刚建立内部记录就打扰外部。后续真正运行状态变化仍会通过普通流程发布。

调用图:调用 1 个内部函数(mutate_and_publish);被 2 处调用(thread_fork_inner, start_detached_review)。

ThreadWatchManager::remove_thread106–110 ↗
async fn remove_thread(&self, thread_id: &str)

作用:把一个线程从状态看板上移走,并把它视为 NotLoaded(未加载)。

数据流:进去线程编号 → 它删除该线程的运行事实,更新订阅者看到的状态 → 出来没有直接返回值,但如果原来不是未加载,会发一条变成 NotLoaded 的通知。

调用关系:线程卸载、清理或特殊事件处理时会调用它。它通过 ThreadWatchManager::mutate_and_publish 统一完成修改和通知。

调用图:调用 1 个内部函数(mutate_and_publish);被 3 处调用(apply_bespoke_event_handling, unload_thread_without_subscribers, finalize_thread_teardown)。

ThreadWatchManager::loaded_status_for_thread112–114 ↗
async fn loaded_status_for_thread(&self, thread_id: &str) -> ThreadStatus

作用:查询某一个线程当前对外应该显示的状态。找不到的线程会被当成未加载。

数据流:进去线程编号 → 它锁住内部状态并查表 → 出来一个 ThreadStatus,比如 Idle、Active、NotLoaded 或 SystemError。

调用关系:很多读取线程详情、恢复线程、更新元数据的流程都会用它来决定该怎么展示或下一步怎么处理。

调用图:被 11 处调用(apply_bespoke_event_handling, handle_pending_thread_resume_request, read_thread_view, resume_running_thread, thread_fork_inner, thread_metadata_update_response_inner, thread_resume_inner, thread_turns_list_response_inner, thread_unarchive_response, start_detached_review (+1 more))。

ThreadWatchManager::loaded_statuses_for_threads116–128 ↗
async fn loaded_statuses_for_threads(
        &self,
        thread_ids: Vec<String>,
    ) -> HashMap<String, ThreadStatus>

作用:一次查询多个线程的状态,适合列表页或搜索结果页批量补状态。

数据流:进去一组线程编号 → 它锁住状态表,逐个查状态,查不到就给 NotLoaded → 出来一个从线程编号到状态的映射表。

调用关系:线程列表和线程搜索响应会用它,避免一个个反复加锁查询。

调用图:被 2 处调用(thread_list_response_inner, thread_search_response_inner)。

ThreadWatchManager::running_turn_count131–139 ↗
async fn running_turn_count(&self) -> usize

作用:测试用函数,用来数当前真正处于 running 的线程回合有几个。

数据流:没有输入 → 它读取内部所有线程的运行事实,只统计 running 为真的项 → 出来一个数字。

调用关系:只在测试里启用,用来验证“等待批准”不应该被误算成正在运行。

ThreadWatchManager::subscribe_running_turn_count141–143 ↗
fn subscribe_running_turn_count(&self) -> watch::Receiver<usize>

作用:让外部订阅“当前有多少线程正在运行”这个数字的变化。

数据流:没有输入 → 它从内部 watch 通道创建一个接收端 → 出来一个 Receiver,订阅者可以拿它等待数字变化。

调用关系:subscribe_running_assistant_turn_count 会调用它。每次状态修改后,ThreadWatchManager::mutate_and_publish 会更新这个数字。

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

ThreadWatchManager::note_turn_started145–152 ↗
async fn note_turn_started(&self, thread_id: &str)

作用:记录某个线程的一轮工作已经开始。这里的 turn 可以理解为一次助手处理请求的回合。

数据流:进去线程编号 → 它把线程标记为已加载、正在运行,并清掉旧的系统错误标记 → 出来没有直接返回值,但状态通常会变成 Active。

调用关系:事件处理流程 apply_bespoke_event_handling 在看到“开始运行”事件时调用它。它通过 ThreadWatchManager::update_runtime_for_thread 更新内部事实。

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

ThreadWatchManager::note_turn_completed154–156 ↗
async fn note_turn_completed(&self, thread_id: &str, _failed: bool)

作用:记录某个线程的一轮工作已经正常结束。

数据流:进去线程编号和失败标记;当前实现不使用失败标记 → 它清掉运行中、等批准、等用户输入这些活动状态 → 出来没有直接返回值,线程通常回到 Idle。

调用关系:事件处理流程在收到完成事件时调用它。它把实际清理交给 ThreadWatchManager::clear_active_state。

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

ThreadWatchManager::note_turn_interrupted158–160 ↗
async fn note_turn_interrupted(&self, thread_id: &str)

作用:记录某个线程的一轮工作被中断了,比如被用户停止。

数据流:进去线程编号 → 它清掉运行中和各种等待中的标记 → 出来没有直接返回值,线程不再显示为活动。

调用关系:事件处理流程在收到中断事件时调用它。它和完成事件一样复用 ThreadWatchManager::clear_active_state。

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

ThreadWatchManager::note_thread_shutdown162–170 ↗
async fn note_thread_shutdown(&self, thread_id: &str)

作用:记录某个线程已经关掉,不能再当作已加载线程看待。

数据流:进去线程编号 → 它把 running、等待批准、等待输入都清零,并把 is_loaded 设为 false → 出来没有直接返回值,状态会变成 NotLoaded。

调用关系:事件处理流程在收到线程关闭信号时调用它。它通过 ThreadWatchManager::update_runtime_for_thread 触发状态刷新和通知。

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

ThreadWatchManager::note_system_error172–180 ↗
async fn note_system_error(&self, thread_id: &str)

作用:记录某个线程遇到了系统级错误,需要显示成错误状态。

数据流:进去线程编号 → 它停止 running,清掉等待事项,并打上 has_system_error 标记 → 出来没有直接返回值,状态会变成 SystemError。

调用关系:事件处理流程在收到系统错误时调用它。下一次 ThreadWatchManager::note_turn_started 会清掉这个错误标记。

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

ThreadWatchManager::clear_active_state182–189 ↗
async fn clear_active_state(&self, thread_id: &str)

作用:统一清理线程的“正在活动”状态,包括正在运行、等批准、等输入。

数据流:进去线程编号 → 它把对应线程的 running 和两个等待计数都归零 → 出来没有直接返回值,状态会重新计算。

调用关系:ThreadWatchManager::note_turn_completed 和 ThreadWatchManager::note_turn_interrupted 都用它,避免两处写重复清理逻辑。

调用图:调用 1 个内部函数(update_runtime_for_thread);被 2 处调用(note_turn_completed, note_turn_interrupted)。

ThreadWatchManager::note_permission_requested191–197 ↗
async fn note_permission_requested(
        &self,
        thread_id: &str,
    ) -> ThreadWatchActiveGuard

作用:记录某个线程正在等待用户批准,比如批准某个操作。

数据流:进去线程编号 → 它增加“等待批准”的计数 → 出来一个 ThreadWatchActiveGuard,调用方保留它期间,状态会带 WaitingOnApproval 标记。

调用关系:事件处理流程在发现需要权限时调用它。它把通用的计数和 guard 创建交给 ThreadWatchManager::note_pending_request。

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

ThreadWatchManager::note_user_input_requested199–205 ↗
async fn note_user_input_requested(
        &self,
        thread_id: &str,
    ) -> ThreadWatchActiveGuard

作用:记录某个线程正在等待用户输入,比如需要用户补一句话或填写信息。

数据流:进去线程编号 → 它增加“等待用户输入”的计数 → 出来一个 ThreadWatchActiveGuard,guard 释放后等待计数会自动减少。

调用关系:事件处理流程在发现需要用户输入时调用它。它复用 ThreadWatchManager::note_pending_request。

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

ThreadWatchManager::note_pending_request207–219 ↗
async fn note_pending_request(
        &self,
        thread_id: &str,
        guard_type: ThreadWatchActiveGuardType,
    ) -> ThreadWatchActiveGuard

作用:处理“有一项等待事项开始了”的通用逻辑,不管是等批准还是等输入。

数据流:进去线程编号和等待类型 → 它把线程标记为已加载,并把对应等待计数加一 → 出来一个 guard,用来在等待结束时自动扣回计数。

调用关系:ThreadWatchManager::note_permission_requested 和 ThreadWatchManager::note_user_input_requested 都调用它。它内部先更新运行事实,再用 ThreadWatchActiveGuard::new 生成自动收尾工具。

调用图:调用 2 个内部函数(new, update_runtime_for_thread);被 2 处调用(note_permission_requested, note_user_input_requested)。

ThreadWatchManager::mutate_and_publish221–244 ↗
async fn mutate_and_publish(&self, mutate: F)

作用:这是本文件的“改状态并通知”的总闸门。所有会改变状态的操作都尽量从这里过,保证改完以后订阅者和外部消息都能跟上。

数据流:进去一个修改函数 → 它先锁住状态执行修改,再统计正在运行的线程数量,释放锁后更新运行数量通道,并在需要时发送 ThreadStatusChanged 通知 → 出来没有直接返回值,但内部状态和外部观察结果都会更新。

调用关系:upsert、remove、update_runtime_for_thread 都调用它。它把 ThreadWatchState 的纯状态修改和 OutgoingMessageSender 的对外通知连接起来。

调用图:被 4 处调用(remove_thread, update_runtime_for_thread, upsert_thread, upsert_thread_silently);外部调用 2 个(send, ThreadStatusChanged)。

ThreadWatchManager::subscribe246–251 ↗
async fn subscribe(
        &self,
        thread_id: ThreadId,
    ) -> Option<watch::Receiver<ThreadStatus>>

作用:让调用者订阅某一个线程的状态变化。

数据流:进去一个 ThreadId → 它转成字符串,锁住状态并取得该线程专用的 watch 接收端 → 出来一个 Receiver,调用者能看到该线程最新状态和后续变化。

调用关系:需要实时跟踪单个线程的地方会调用它。底层工作由 ThreadWatchState::subscribe 完成。

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

ThreadWatchManager::note_active_guard_released253–263 ↗
async fn note_active_guard_released(
        &self,
        thread_id: String,
        guard_type: ThreadWatchActiveGuardType,
    )

作用:当等待事项的 guard 被释放时,减少对应的等待计数。

数据流:进去线程编号和等待类型 → 它找到对应计数,用安全减法减一,避免已经是零时变成负数 → 出来没有直接返回值,状态会重新计算。

调用关系:ThreadWatchActiveGuard::drop 会异步触发它。它再通过 ThreadWatchManager::update_runtime_for_thread 更新状态。

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

ThreadWatchManager::update_runtime_for_thread265–272 ↗
async fn update_runtime_for_thread(&self, thread_id: &str, update: F)

作用:给某个线程改一小块运行事实,比如 running、等待计数或错误标记。

数据流:进去线程编号和一个具体修改动作 → 它包装成对 ThreadWatchState 的更新 → 出来没有直接返回值,但会经过统一的发布流程。

调用关系:开始、结束、关闭、报错、等待请求和 guard 释放都会调用它。它把工作交给 ThreadWatchManager::mutate_and_publish。

调用图:调用 1 个内部函数(mutate_and_publish);被 6 处调用(clear_active_state, note_active_guard_released, note_pending_request, note_system_error, note_thread_shutdown, note_turn_started)。

ThreadWatchManager::pending_counter274–282 ↗
fn pending_counter(
        runtime: &mut RuntimeFacts,
        guard_type: ThreadWatchActiveGuardType,
    ) -> &mut u32

作用:根据等待类型,找到该改哪个计数器:等批准,还是等用户输入。

数据流:进去一份线程运行事实和等待类型 → 它选择对应字段 → 出来这个字段的可修改引用,调用者可以加一或减一。

调用关系:ThreadWatchManager::note_pending_request 和 ThreadWatchManager::note_active_guard_released 用它,保证两类等待事项不会改错计数器。

resolve_thread_status285–299 ↗
fn resolve_thread_status(
    status: ThreadStatus,
    has_in_progress_turn: bool,
) -> ThreadStatus

作用:修正一种短暂的时间差:真实工作已经开始了,但状态监听还没来得及看到。

数据流:进去一个已有状态和“是否有正在进行的回合”这个布尔值 → 如果已有状态是 Idle 或 NotLoaded,但确实有回合在跑,就改成 Active → 出来修正后的 ThreadStatus。

调用关系:测试验证了它的行为。它适合用在外层读状态时,避免用户看到线程明明在工作却显示空闲。

调用图:被 1 处调用(resolves_in_progress_turn_to_active_status);外部调用 2 个(new, matches!)。

ThreadWatchState::upsert_thread308–325 ↗
fn upsert_thread(
        &mut self,
        thread_id: String,
        emit_notification: bool,
    ) -> Option<ThreadStatusChangedNotification>

作用:在底层状态表里登记或刷新一个线程,并决定是否生成状态变化通知。

数据流:进去线程编号和是否发通知的开关 → 它先记住旧状态,再把线程设为已加载,更新订阅者,最后按开关决定是否返回通知 → 出来可能是一条 ThreadStatusChangedNotification。

调用关系:ThreadWatchManager::upsert_thread 和静默版本会通过 mutate_and_publish 间接调用它。它属于内部状态层,不直接发消息。

调用图:调用 3 个内部函数(status_changed_notification, status_for, update_status_watcher_for_thread)。

ThreadWatchState::remove_thread327–339 ↗
fn remove_thread(&mut self, thread_id: &str) -> Option<ThreadStatusChangedNotification>

作用:从底层状态表里删除线程,并把订阅者看到的状态改成 NotLoaded。

数据流:进去线程编号 → 它读取旧状态,删除运行事实,更新 watch 订阅者 → 出来可能是一条变成 NotLoaded 的通知。

调用关系:ThreadWatchManager::remove_thread 通过统一发布流程调用它。它只负责算出该不该通知,不负责真正发送。

调用图:调用 2 个内部函数(status_for, update_status_watcher)。

ThreadWatchState::update_runtime341–358 ↗
fn update_runtime(
        &mut self,
        thread_id: &str,
        mutate: F,
    ) -> Option<ThreadStatusChangedNotification>

作用:修改某个线程的运行事实,并根据新旧状态判断是否需要通知。

数据流:进去线程编号和修改函数 → 它保存旧状态,确保线程记录存在并标记已加载,执行修改,更新订阅者,再比较新旧状态 → 出来可能是一条状态变化通知。

调用关系:ThreadWatchManager::update_runtime_for_thread 间接调用它。几乎所有运行事件最终都会落到这里。

调用图:调用 3 个内部函数(status_changed_notification, status_for, update_status_watcher_for_thread)。

ThreadWatchState::status_for360–364 ↗
fn status_for(&self, thread_id: &str) -> Option<ThreadStatus>

作用:只在内部状态表里查某个线程的状态;如果表里没有,就返回没有结果。

数据流:进去线程编号 → 它查 runtime_by_thread_id,并把 RuntimeFacts 转成 ThreadStatus → 出来 Option<ThreadStatus>,表示可能有也可能没有。

调用关系:多个内部函数用它比较新旧状态。真正对外查询通常用 loaded_status_for_thread,因为它会把缺失值补成 NotLoaded。

调用图:被 5 处调用(loaded_status_for_thread, remove_thread, status_changed_notification, update_runtime, upsert_thread)。

ThreadWatchState::loaded_status_for_thread366–369 ↗
fn loaded_status_for_thread(&self, thread_id: &str) -> ThreadStatus

作用:内部版的“查询线程状态”,并把没记录的线程统一看成 NotLoaded。

数据流:进去线程编号 → 它先调用 status_for → 如果没有记录,就补 ThreadStatus::NotLoaded → 出来一个确定的状态。

调用关系:订阅初始化和更新订阅者时都会用它,保证接收者总能拿到一个明确状态。

调用图:调用 1 个内部函数(status_for);被 2 处调用(subscribe, update_status_watcher_for_thread)。

ThreadWatchState::subscribe371–378 ↗
fn subscribe(&mut self, thread_id: String) -> watch::Receiver<ThreadStatus>

作用:为某个线程创建或复用一个状态订阅通道。

数据流:进去线程编号 → 它先算当前状态,如果这个线程还没有发送端就创建一个 watch 通道 → 出来一个接收端,订阅者可以监听后续变化。

调用关系:ThreadWatchManager::subscribe 会调用它。之后 ThreadWatchState::update_status_watcher 会向这些订阅者推送变化。

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

ThreadWatchState::update_status_watcher_for_thread380–383 ↗
fn update_status_watcher_for_thread(&mut self, thread_id: &str)

作用:把某个线程当前算出来的状态推给它的订阅者。

数据流:进去线程编号 → 它先重新计算该线程状态 → 再调用 update_status_watcher 真正发送 → 出来没有直接返回值。

调用关系:线程被登记或运行事实改变后会调用它。它是“状态表变化”和“watch 订阅者收到更新”之间的连接点。

调用图:调用 2 个内部函数(loaded_status_for_thread, update_status_watcher);被 2 处调用(update_runtime, upsert_thread)。

ThreadWatchState::update_status_watcher385–403 ↗
fn update_status_watcher(&mut self, thread_id: &str, status: &ThreadStatus)

作用:向某个线程的 watch 通道发送新状态,并清理没人听的通道。

数据流:进去线程编号和新状态 → 如果有发送端,它只在状态真的不同的时候更新;然后检查接收者数量 → 如果没人订阅,就把发送端从表里删掉。

调用关系:ThreadWatchState::remove_thread 和 update_status_watcher_for_thread 会调用它。这样订阅系统不会积累没人使用的旧通道。

调用图:被 2 处调用(remove_thread, update_status_watcher_for_thread);外部调用 1 个(clone)。

ThreadWatchState::status_changed_notification405–417 ↗
fn status_changed_notification(
        &self,
        thread_id: String,
        previous_status: Option<ThreadStatus>,
    ) -> Option<ThreadStatusChangedNotification>

作用:根据旧状态和当前状态,判断是否需要生成一条对外状态变化通知。

数据流:进去线程编号和旧状态 → 它查当前状态;如果当前没有记录或和旧状态一样,就不通知 → 否则出来一条 ThreadStatusChangedNotification。

调用关系:upsert 和 update_runtime 用它。真正发送通知由更外层的 ThreadWatchManager::mutate_and_publish 完成。

调用图:调用 1 个内部函数(status_for);被 2 处调用(update_runtime, upsert_thread)。

loaded_thread_status429–451 ↗
fn loaded_thread_status(runtime: &RuntimeFacts) -> ThreadStatus

作用:把内部零散事实翻译成人能理解、协议能发送的线程状态。

数据流:进去 RuntimeFacts,比如是否加载、是否运行、等待计数、是否系统错误 → 它按优先级判断:未加载优先;运行或有等待就是 Active;系统错误就是 SystemError;否则 Idle → 出来一个 ThreadStatus。

调用关系:ThreadWatchState::status_for 会用它。它是状态判断的核心规则表。

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

tests::loaded_status_defaults_to_not_loaded_for_untracked_threads466–475 ↗
async fn loaded_status_defaults_to_not_loaded_for_untracked_threads()

作用:测试没登记过的线程会不会默认显示为 NotLoaded。

数据流:进去一个新管理器和一个不存在的线程编号 → 它查询状态 → 期望出来 NotLoaded。

调用关系:它验证 ThreadWatchManager::new 和 loaded_status_for_thread 的默认行为,防止未知线程被误显示为空闲或活动。

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

tests::tracks_non_interactive_thread_status478–497 ↗
async fn tracks_non_interactive_thread_status()

作用:测试非交互线程开始运行后也会显示为 Active。

数据流:进去一个测试线程 → 它登记线程,再记录回合开始 → 查询结果应从 Idle 变成 Active。

调用关系:它覆盖 ThreadWatchManager::upsert_thread、note_turn_started 和 loaded_status_for_thread 的配合。

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

tests::status_updates_track_single_thread500–575 ↗
async fn status_updates_track_single_thread()

作用:测试单个交互线程从运行、等批准、等输入,到恢复空闲的完整状态变化。

数据流:进去一个测试线程 → 它依次模拟开始运行、请求权限、请求用户输入、释放两个 guard、完成回合 → 每一步都检查状态和活动标记是否正确。

调用关系:它把 note_turn_started、note_permission_requested、note_user_input_requested、guard 自动释放和 note_turn_completed 串起来验一遍。

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

tests::resolves_in_progress_turn_to_active_status578–595 ↗
fn resolves_in_progress_turn_to_active_status()

作用:测试当外部已知有回合正在进行时,Idle 或 NotLoaded 会被修正成 Active。

数据流:进去 Idle 和 NotLoaded 两种状态,并把 has_in_progress_turn 设为真 → 调用 resolve_thread_status → 期望都出来 Active。

调用关系:它直接验证 resolve_thread_status 解决短暂状态延迟的规则。

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

tests::keeps_status_when_no_in_progress_turn598–610 ↗
fn keeps_status_when_no_in_progress_turn()

作用:测试没有正在进行的回合时,状态不会被乱改。

数据流:进去 Idle 或 SystemError,并把 has_in_progress_turn 设为假 → 比较结果 → 期望输出仍是原状态。

调用关系:它补充验证 resolve_thread_status 的边界:只有确实有运行中回合时才修正。

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

tests::system_error_sets_idle_flag_until_next_turn613–641 ↗
async fn system_error_sets_idle_flag_until_next_turn()

作用:测试系统错误会显示为 SystemError,并且下一次开始运行会清掉这个错误。

数据流:进去一个测试线程 → 它登记、开始运行、记录系统错误,再查询状态;随后再次开始运行 → 结果先是 SystemError,后是 Active。

调用关系:它验证 note_system_error 和 note_turn_started 的配合,确保旧错误不会污染下一轮运行。

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

tests::shutdown_marks_thread_not_loaded644–662 ↗
async fn shutdown_marks_thread_not_loaded()

作用:测试线程关闭后会变成 NotLoaded。

数据流:进去一个测试线程 → 它登记并开始运行,然后记录 shutdown → 查询结果应为 NotLoaded。

调用关系:它验证 ThreadWatchManager::note_thread_shutdown 会清掉加载状态和活动状态。

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

tests::loaded_statuses_default_to_not_loaded_for_untracked_threads665–692 ↗
async fn loaded_statuses_default_to_not_loaded_for_untracked_threads()

作用:测试批量查询时,已知线程和未知线程都会得到正确状态。

数据流:进去两个线程编号,其中一个已登记并正在运行,另一个不存在 → 批量查询 → 结果中一个是 Active,另一个是 NotLoaded。

调用关系:它验证 ThreadWatchManager::loaded_statuses_for_threads 适合列表场景,不会漏掉未知线程的默认状态。

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

tests::has_running_turns_tracks_runtime_running_flag_only695–718 ↗
async fn has_running_turns_tracks_runtime_running_flag_only()

作用:测试“正在运行的线程数量”只看 running 标记,不把等待批准误算进去。

数据流:进去一个测试线程 → 先请求权限,再开始运行,再完成回合 → 每一步检查 running_turn_count 是否是 0、1、0。

调用关系:它验证运行数量统计和活动状态标记不是一回事,避免界面或外部订阅误判。

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

tests::status_change_emits_notification721–761 ↗
async fn status_change_emits_notification()

作用:测试状态变化时会向外发 ThreadStatusChanged 通知。

数据流:进去一个带 outgoing 通道的管理器 → 它登记线程、开始运行、删除线程 → 从接收端依次收到 Idle、Active、NotLoaded 通知。

调用关系:它验证 ThreadWatchManager::new_with_outgoing 和 mutate_and_publish 的对外广播行为。

调用图:调用 3 个内部函数(disabled, new, new_with_outgoing);外部调用 4 个(new, test_thread, assert_eq!, channel)。

tests::silent_upsert_skips_initial_notification764–801 ↗
async fn silent_upsert_skips_initial_notification()

作用:测试静默登记不会发送初始通知,但之后状态变化仍会发送通知。

数据流:进去一个带 outgoing 通道的管理器 → 静默登记线程后检查收不到消息;再开始运行 → 应收到 Active 通知。

调用关系:它验证 upsert_thread_silently 只跳过第一次通知,不会关闭后续通知机制。

调用图:调用 3 个内部函数(disabled, new, new_with_outgoing);外部调用 5 个(new, test_thread, assert!, assert_eq!, channel)。

tests::status_watchers_receive_only_their_thread_updates804–850 ↗
async fn status_watchers_receive_only_their_thread_updates()

作用:测试订阅某个线程的人只会收到这个线程的变化,不会被别的线程打扰。

数据流:进去两个测试线程和两个订阅者 → 只让第一个线程开始运行 → 第一个订阅者收到 Active,第二个订阅者没有收到变化且仍是 Idle。

调用关系:它验证 ThreadWatchManager::subscribe、ThreadWatchState::subscribe 和 update_status_watcher 的按线程隔离。

调用图:调用 2 个内部函数(new, from_string);外部调用 5 个(from_secs, test_thread, assert!, assert_eq!, timeout)。

tests::wait_for_status852–868 ↗
async fn wait_for_status(
        manager: &ThreadWatchManager,
        thread_id: &str,
        expected_status: ThreadStatus,
    )

作用:测试辅助函数,反复等待某个线程变成指定状态。

数据流:进去管理器、线程编号和期望状态 → 它在超时时间内循环查询,不符合就让出执行机会 → 成功时正常结束,超时则测试失败。

调用关系:状态 guard 的释放是异步发生的,所以 tests::status_updates_track_single_thread 用它等待状态真正更新。

调用图:调用 1 个内部函数(loaded_status_for_thread);外部调用 3 个(from_secs, yield_now, timeout)。

tests::recv_status_changed_notification870–887 ↗
async fn recv_status_changed_notification(
        outgoing_rx: &mut mpsc::Receiver<OutgoingEnvelope>,
    ) -> ThreadStatusChangedNotification

作用:测试辅助函数,从 outgoing 通道里取出并确认是一条线程状态变化通知。

数据流:进去一个消息接收端 → 它限时接收一条消息,检查消息形状必须是广播的 ThreadStatusChanged → 出来具体的 ThreadStatusChangedNotification。

调用关系:通知相关测试用它来避免重复写拆消息和检查消息类型的代码。

调用图:调用 1 个内部函数(recv);外部调用 3 个(from_secs, panic!, timeout)。

tests::test_thread889–912 ↗
fn test_thread(thread_id: &str, source: codex_app_server_protocol::SessionSource) -> Thread

作用:测试辅助函数,快速造一个内容完整但数据假的 Thread。

数据流:进去线程编号和来源类型 → 它填入测试用的 session、路径、模型、版本、初始状态等字段 → 出来一个可交给管理器登记的 Thread。

调用关系:多数测试用它准备输入数据。这样测试重点放在线程状态变化,而不是手写一大堆 Thread 字段。

调用图:外部调用 3 个(new, new, test_path_buf)。

core/src/review_format.rs源码 ↗
domain_logicreview output rendering

代码审查工具给出的结果通常是结构化数据:哪里有问题、标题是什么、正文说明是什么、总体解释是什么。这个文件做的事,就是把这些“零散字段”拼成用户能直接看的文字。它不负责上色,也不关心界面长什么样,所以叫“UI 无关”:就像先把菜单内容写好,餐厅再决定用纸质菜单还是电子屏展示。它会先把每条发现的问题变成“标题 — 文件路径:起始行-结束行”的形式;如果需要让用户勾选,还会加上“[x]”或“[ ]”。最后,它会把总体说明和问题列表合在一起。如果审查器什么都没给,它不会返回空白,而是给出一句兜底提示,避免用户看到一片空。

函数细节3
format_location7–12 ↗
fn format_location(item: &ReviewFinding) -> String

作用:把一条审查发现里的代码位置,变成一段短文字,比如“某个文件:第几行-第几行”。这样用户一眼就知道问题出在哪里。

数据流:进去的是一条 ReviewFinding,也就是一条审查发现;函数读取里面的绝对文件路径和行号范围;然后把它们拼成“路径:开始行-结束行”的字符串;出来的是这段位置文字,不改动原来的审查数据。

调用关系:它是一个小零件,专门给 format_review_findings_block 使用。后者在生成完整问题列表时,每处理一条发现都会叫它来写出位置。

调用图:被 1 处调用(format_review_findings_block);外部调用 1 个(format!)。

format_review_findings_block23–58 ↗
fn format_review_findings_block(
    findings: &[ReviewFinding],
    selection: Option<&[bool]>,
) -> String

作用:把多条审查发现整理成一整块可读的文字列表。有人需要把审查结果展示给用户时,就可以直接拿它生成列表内容。

数据流:进去的是一组 ReviewFinding,以及可选的 selection 勾选状态;函数先决定标题是单数“Review comment”还是复数“Full review comments”;再逐条读取每个问题的标题、正文和位置;位置交给 format_location 拼好;如果传入了勾选状态,就给每条加上“[x]”或“[ ]”,缺失的勾选值默认当作已选;最后出来的是用换行连接好的一整段文本,不会修改输入的数据。

调用关系:它处在“审查数据”到“用户可见文字”的中间层。render_review_output_text 会用它把 findings 部分拼进最终说明;退出审查模式的流程 exit_review_mode 也会直接用它来展示或处理完整审查列表。

调用图:调用 1 个内部函数(format_location);被 3 处调用(render_review_output_text, render_review_output_text, exit_review_mode);外部调用 5 个(new, new, format!, iter, len)。

render_review_output_text64–82 ↗
fn render_review_output_text(output: &ReviewOutputEvent) -> String

作用:把一次审查输出变成最终要给用户看的完整文字。它会同时照顾总体解释和具体问题列表,还会在没有内容时给出友好的失败提示。

数据流:进去的是 ReviewOutputEvent,也就是一次审查输出事件;函数先读取 overall_explanation,并去掉前后空白;如果解释不为空,就放进结果段落;再检查 findings,如果有具体问题,就调用 format_review_findings_block 生成问题列表并加入结果;如果两边都没有内容,就返回固定兜底句“Reviewer failed to output a response.”;出来的是最终文本,不改动事件本身。

调用关系:它是这个文件里最接近外部使用的入口。退出审查模式 exit_review_mode 会调用它,把审查器的输出变成用户消息;测试 review_op_emits_lifecycle_and_review_output 也会用它来确认审查输出能按预期生成。它把具体列表格式化的工作交给 format_review_findings_block。

调用图:调用 1 个内部函数(format_review_findings_block);被 2 处调用(exit_review_mode, review_op_emits_lifecycle_and_review_output);外部调用 1 个(new)。

app-server-protocol/src/protocol/event_mapping.rs源码 ↗
domain_logicevent handling

核心系统会产生很多不同事件,比如工具调用完成、命令开始执行、命令输出了一段文字、协作代理开始或结束工作等。客户端不能直接吃这些内部事件,因为字段名、状态表达、数据形状都不一定一样。这个文件的主要函数 item_event_to_server_notification 就负责做这层转换:拿到一个 EventMsg,再加上线程 ID 和回合 ID,然后按事件类型拼出对应的 ServerNotification。它会把“开始”事件变成 ItemStarted,把“结束”事件变成 ItemCompleted,也会把失败状态、接收方线程、命令输出、文件补丁变化等信息整理好。文件底部的测试专门检查几个容易出错的映射,比如恢复代理、命令输出增量,确保翻译结果不跑偏。

函数细节7
item_event_to_server_notification30–464 ↗
fn item_event_to_server_notification(
    msg: EventMsg,
    thread_id: &str,
    turn_id: &str,
) -> ServerNotification

作用:把一个核心事件转换成服务器要发给客户端的通知。有人会在收到内部事件后调用它,好让客户端能用统一的 v2 协议看到这件事。

数据流:输入是一个 EventMsg,以及当前 thread_id 和 turn_id。函数先把线程和回合编号转成字符串,然后根据事件种类分别处理:比如工具调用结果会变成完成通知,协作代理开始会变成开始通知,命令输出的字节会转成文字增量,文件补丁会转成客户端格式。输出是一个 ServerNotification;它不保存额外状态,只负责把这一条事件翻译成这一条通知。

调用关系:这是本文件的核心转换器。测试函数 collab_resume_begin_maps_to_item_started_resume_agent、collab_resume_end_maps_to_item_completed_resume_agent 和 exec_command_output_delta_maps_to_command_execution_output_delta 会直接调用它来验证结果。转换过程中,它会把命令执行事件交给 build_command_execution_begin_item 和 build_command_execution_end_item 来组装条目,把补丁变化交给 convert_patch_changes 来改成客户端需要的形状。

调用图:调用 4 个内部函数(build_command_execution_begin_item, build_command_execution_end_item, convert_patch_changes, from);被 3 处调用(collab_resume_begin_maps_to_item_started_resume_agent, collab_resume_end_maps_to_item_completed_resume_agent, exec_command_output_delta_maps_to_command_execution_output_delta);外部调用 17 个(new, AgentMessageDelta, CommandExecutionOutputDelta, FileChangePatchUpdated, ItemCompleted, ItemStarted, PlanDelta, ReasoningSummaryPartAdded, ReasoningSummaryTextDelta, ReasoningTextDelta (+7 more))。

tests::assert_item_started_server_notification476–484 ↗
fn assert_item_started_server_notification(
        notification: ServerNotification,
        expected: ItemStartedNotification,
    )

作用:这是测试里的小帮手,用来确认某个通知确实是“条目开始了”的通知,而且内容完全符合预期。

数据流:输入是实际生成的 ServerNotification 和期望的 ItemStartedNotification。它先检查实际通知的类型是不是 ItemStarted;如果是,就比较里面的内容是否一样;如果不是,就直接让测试失败并报错。

调用关系:它服务于测试流程,主要被 collab_resume_begin_maps_to_item_started_resume_agent 使用。这样测试用例不用反复写匹配和比较代码,只要把期望结果交给这个帮手检查即可。

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

tests::assert_item_completed_server_notification486–494 ↗
fn assert_item_completed_server_notification(
        notification: ServerNotification,
        expected: ItemCompletedNotification,
    )

作用:这是测试里的小帮手,用来确认某个通知确实是“条目完成了”的通知,并且完成内容和预期一致。

数据流:输入是实际通知和期望的完成通知。它检查实际通知是不是 ItemCompleted;如果是,就逐项比较;如果拿到的是别的通知类型,就让测试失败并说明拿错了类型。

调用关系:它被 collab_resume_end_maps_to_item_completed_resume_agent 使用,用来检查协作代理恢复结束后,转换出的完成通知是否正确。它让测试代码更短,也让失败信息更清楚。

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

tests::assert_command_execution_output_delta_server_notification496–506 ↗
fn assert_command_execution_output_delta_server_notification(
        notification: ServerNotification,
        expected: CommandExecutionOutputDeltaNotification,
    )

作用:这是测试里的小帮手,用来确认某个通知是“命令执行输出了一小段内容”的通知,并且输出文本正确。

数据流:输入是实际生成的 ServerNotification 和期望的 CommandExecutionOutputDeltaNotification。它先判断通知类型是不是 CommandExecutionOutputDelta;是的话就比较内容,不是的话就让测试失败。

调用关系:它被 exec_command_output_delta_maps_to_command_execution_output_delta 使用。这个测试关心的是命令输出的字节是否被正确变成客户端能显示的文字,所以这个帮手专门检查这类通知。

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

tests::collab_resume_begin_maps_to_item_started_resume_agent509–543 ↗
fn collab_resume_begin_maps_to_item_started_resume_agent()

作用:这个测试确认“恢复协作代理开始”这种内部事件,会被翻译成“恢复代理工具调用正在进行”的开始通知。

数据流:测试先造出一个 CollabResumeBeginEvent,里面有调用 ID、开始时间、发送方线程和接收方线程。然后把它包成 EventMsg 交给 item_event_to_server_notification。最后用 assert_item_started_server_notification 检查输出:线程号、回合号、工具类型、进行中状态、接收方线程等都必须符合预期。

调用关系:这是对核心转换器 item_event_to_server_notification 的定点检查。它验证恢复代理的 begin 分支没有被误映射成别的工具,也没有漏掉发送方和接收方线程信息。

调用图:调用 2 个内部函数(item_event_to_server_notification, new);外部调用 4 个(new, assert_item_started_server_notification, CollabResumeBegin, vec!)。

tests::collab_resume_end_maps_to_item_completed_resume_agent546–587 ↗
fn collab_resume_end_maps_to_item_completed_resume_agent()

作用:这个测试确认“恢复协作代理结束”这种内部事件,会被翻译成完成通知;如果目标代理没找到,状态应当是失败。

数据流:测试先造出一个 CollabResumeEndEvent,并把状态设为 NotFound,也就是没找到代理。它把事件送进 item_event_to_server_notification 后,期望得到一个 ItemCompleted 通知,里面的工具是 ResumeAgent,整体状态是 Failed,同时记录接收方代理的状态。

调用关系:它检查 item_event_to_server_notification 里恢复代理 end 分支的失败判断。这个测试很重要,因为如果 NotFound 被误当成成功,客户端就会以为代理恢复了,实际却没有。

调用图:调用 3 个内部函数(item_event_to_server_notification, from, new);外部调用 3 个(assert_item_completed_server_notification, CollabResumeEnd, vec!)。

tests::exec_command_output_delta_maps_to_command_execution_output_delta590–610 ↗
fn exec_command_output_delta_maps_to_command_execution_output_delta()

作用:这个测试确认命令执行过程中吐出的一小段输出,会被翻译成客户端可显示的命令输出增量通知。

数据流:测试构造一个 ExecCommandOutputDeltaEvent,里面的输出内容是字节形式的 hello。它调用 item_event_to_server_notification 后,期望得到 CommandExecutionOutputDelta 通知,并且 item_id 是调用 ID,delta 是字符串 hello。

调用关系:它验证 item_event_to_server_notification 处理命令输出的分支。这个分支会把原始字节转换成文字;测试通过 assert_command_execution_output_delta_server_notification 来确认转换后的通知类型和内容都正确。

调用图:调用 1 个内部函数(item_event_to_server_notification);外部调用 2 个(assert_command_execution_output_delta_server_notification, ExecCommandOutputDelta)。

app-server-protocol/src/protocol/item_builders.rs源码 ↗
domain_logicrequest handling / cross-cutting

底层协议里有很多比较原始的事件,比如“准备改文件了”“命令开始跑了”“守护检查通过了”。客户端不想直接读这些碎片,它更需要一条条适合展示的记录。这个文件就像一个“翻译柜台”:收到核心事件后,整理成 ThreadItem,比如文件变更卡片、命令执行卡片、审批结果通知。它会补上状态、命令文本、工作目录、退出码、耗时、文件 diff(改动内容)等展示所需信息。这里还有一个重要点:有些工具其实还没真正开始执行,比如还在等审批,但界面仍然要显示“将要做什么”。所以这些 builder 会造出“合成”的条目,让用户提前看见风险和内容。文件改动会被按路径排序,保证显示顺序稳定;命令参数会被拼成一条 shell 命令,方便人读。

函数细节11
build_file_change_approval_request_item40–48 ↗
fn build_file_change_approval_request_item(
    payload: &ApplyPatchApprovalRequestEvent,
) -> ThreadItem

作用:把“申请修改文件、等待批准”的底层事件,做成一条客户端能显示的文件变更条目。用户还没批准时,界面也能先看到准备改哪些文件。

数据流:输入是一份 ApplyPatchApprovalRequestEvent,里面有调用编号和文件改动清单。函数把调用编号当作条目 id,把文件改动交给 convert_patch_changes 转成前端格式,并把状态设成 InProgress,也就是“进行中/等待中”。输出是一条 ThreadItem::FileChange,不会真正改文件,只是生成展示用数据。

调用关系:当 handle_apply_patch_approval_request 收到补丁审批请求时会调用它。它自己不整理每个文件的细节,而是把这部分交给 convert_patch_changes,保证审批阶段和真正执行阶段的文件展示格式一致。

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

build_file_change_begin_item50–56 ↗
fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItem

作用:把“开始应用文件补丁”的事件,做成一条文件变更条目。这样客户端能在补丁刚开始时立刻显示正在改哪些文件。

数据流:输入是 PatchApplyBeginEvent,包含调用编号和文件变化。函数复制 id,把文件变化转换成 FileUpdateChange 列表,并把状态标成 InProgress。输出是一个 ThreadItem::FileChange,表示这批文件改动正在发生。

调用关系:handle_patch_apply_begin 在收到补丁开始事件时会用它。它和审批请求的构造方式很像,都是调用 convert_patch_changes,所以界面不会因为事件来源不同而显示出两套格式。

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

build_file_change_end_item58–64 ↗
fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem

作用:把“文件补丁应用结束”的事件,做成最终版文件变更条目。它告诉客户端这次改文件是成功、失败,还是其他结束状态。

数据流:输入是 PatchApplyEndEvent,里面有调用编号、文件改动和底层状态。函数转换文件改动列表,再把底层状态换成 app-server 协议里的 PatchApplyStatus。输出是一条 ThreadItem::FileChange,状态不再固定为进行中,而是反映实际结束结果。

调用关系:handle_patch_apply_end 在补丁结束时调用它。它继续复用 convert_patch_changes 来生成文件清单,自己重点负责把“结束状态”带到 ThreadItem 里。

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

build_command_execution_approval_request_item66–86 ↗
fn build_command_execution_approval_request_item(
    payload: &ExecApprovalRequestEvent,
) -> ThreadItem

作用:把“命令执行前需要审批”的请求,做成一条命令执行条目。即使命令还没真正运行,客户端也能先显示将要运行什么、在哪个目录运行。

数据流:输入是 ExecApprovalRequestEvent,包含命令参数、工作目录、调用编号和解析后的命令动作。函数用 shlex_join 把参数列表拼成一条人能读的命令字符串,把来源标成 Agent,把状态标成 InProgress,并把解析结果转换成 command_actions。输出是一条 ThreadItem::CommandExecution;进程号、输出、退出码和耗时都为空,因为命令还没开始。

调用关系:它服务于命令审批场景。它会调用 shlex_join 来把参数数组变成类似终端里看到的一行命令;命令动作则通过 CommandAction::from_core_with_cwd 转成客户端能展示的动作说明。

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

build_command_execution_begin_item88–106 ↗
fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem

作用:把“命令开始执行”的事件,做成一条进行中的命令执行条目。客户端可以用它显示命令、目录、进程号和当前运行状态。

数据流:输入是 ExecCommandBeginEvent,包含命令参数、工作目录、进程号、来源和解析后的动作。函数拼出命令字符串,复制 cwd 和 process_id,把状态设为 InProgress,并转换 command_actions。输出是一条 ThreadItem::CommandExecution,输出、退出码和耗时暂时为空,因为命令还没结束。

调用关系:item_event_to_server_notification 和 handle_exec_command_begin 会在命令开始时调用它。它用 shlex_join 生成展示命令,并把已解析的命令动作转给 CommandAction::from_core_with_cwd,让客户端知道这条命令大概会做什么。

调用图:调用 1 个内部函数(shlex_join);被 2 处调用(item_event_to_server_notification, handle_exec_command_begin)。

build_command_execution_end_item108–133 ↗
fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItem

作用:把“命令执行结束”的事件,做成带结果的命令执行条目。它让客户端看到命令输出、退出码、耗时以及最终成功或失败状态。

数据流:输入是 ExecCommandEndEvent,包含命令、目录、进程号、状态、输出、退出码和运行时间。函数把空输出变成 None,避免显示没内容的输出框;把耗时从毫秒数转换成 i64,太大时用最大值兜底;再组装成 ThreadItem::CommandExecution。输出是一条完整的结束态命令记录。

调用关系:item_event_to_server_notification 和 handle_exec_command_end 会在命令结束时调用它。它调用 shlex_join 来生成可读命令,并使用 try_from 做耗时数值转换,防止时间数值超出协议字段能装下的范围。

调用图:调用 1 个内部函数(shlex_join);被 2 处调用(item_event_to_server_notification, handle_exec_command_end);外部调用 1 个(try_from)。

build_item_from_guardian_event139–204 ↗
fn build_item_from_guardian_event(
    assessment: &GuardianAssessmentEvent,
    status: CommandExecutionStatus,
) -> Option<ThreadItem>

作用:把 Guardian 守护检查事件转成可显示的命令执行条目。Guardian 可以理解为一道自动安全检查门,它判断某个动作是否安全;这个函数让这类检查也能在会话里显示成用户看得懂的记录。

数据流:输入是 GuardianAssessmentEvent 和想要显示的命令状态。函数先看被检查的动作类型:如果是普通命令,就直接拿命令文本和目录生成 CommandExecution;如果是 execve,也就是更底层的“运行某个程序加参数”,它会把程序和参数重新拼成一条命令,并尝试解析出动作;如果是补丁、网络访问、工具调用、权限请求等非命令动作,就返回 None。输出可能是一条 ThreadItem,也可能什么都不生成。

调用关系:handle_guardian_assessment 在处理 Guardian 检查结果时调用它。它会用 shlex_join 拼命令,用 parse_command 尝试理解命令含义;如果理解不了,就退回到 Unknown 动作,至少保证界面还能显示原始命令。

调用图:调用 2 个内部函数(parse_command, shlex_join);被 1 处调用(handle_guardian_assessment);外部调用 2 个(once, vec!)。

guardian_auto_approval_review_notification206–277 ↗
fn guardian_auto_approval_review_notification(
    conversation_id: &ThreadId,
    event_turn_id: &str,
    assessment: &GuardianAssessmentEvent,
) -> ServerNotification

作用:把 Guardian 自动审批检查,包装成发给客户端的通知。它告诉客户端一次审批检查是开始了,还是已经批准、拒绝、超时或中止了。

数据流:输入是会话 id、当前事件的 turn id,以及 GuardianAssessmentEvent。函数先决定使用哪个 turn id:事件里有就用事件里的,没有就用传进来的。然后把底层状态、风险等级、用户授权、理由、动作等信息整理成 GuardianApprovalReview。最后按状态输出两类 ServerNotification:进行中就是 Started,结束状态就是 Completed,并补上完成时间和决策来源。

调用关系:它位于 Guardian 审批信息发给客户端之前的最后转换步骤。它会构造 ItemGuardianApprovalReviewStartedNotification 或 ItemGuardianApprovalReviewCompletedNotification,并把 ThreadId 转成字符串,方便协议传输。

调用图:外部调用 3 个(ItemGuardianApprovalReviewCompleted, ItemGuardianApprovalReviewStarted, to_string)。

convert_patch_changes279–290 ↗
fn convert_patch_changes(changes: &HashMap<PathBuf, FileChange>) -> Vec<FileUpdateChange>

作用:把底层的文件改动表,转成客户端需要的文件改动列表。它还会按文件路径排序,让同一批改动每次显示顺序都稳定。

数据流:输入是一个 HashMap,里面用文件路径对应具体 FileChange。函数逐个取出路径和改动,把路径转成字符串,把改动类型交给 map_patch_change_kind,把具体内容交给 format_file_change_diff,然后收集成 Vec<FileUpdateChange。最后按 path 排序并输出这个列表。

调用关系:它被 item_event_to_server_notification、build_file_change_approval_request_item、build_file_change_begin_item、build_file_change_end_item 和 from 等路径复用。它是文件变更展示格式的共同入口,避免不同地方各自转换导致显示不一致。

调用图:被 5 处调用(item_event_to_server_notification, build_file_change_approval_request_item, build_file_change_begin_item, build_file_change_end_item, from)。

map_patch_change_kind292–300 ↗
fn map_patch_change_kind(change: &FileChange) -> PatchChangeKind

作用:判断一个文件改动到底是新增、删除,还是更新。更新时如果包含移动路径,也会把“移动到哪里”带上。

数据流:输入是一个 FileChange。函数匹配它的具体种类:Add 转成 PatchChangeKind::Add,Delete 转成 PatchChangeKind::Delete,Update 转成 PatchChangeKind::Update,并复制 move_path。输出是客户端协议里的改动类型。

调用关系:它是 convert_patch_changes 里的小零件,专门负责“改动类别”的翻译。convert_patch_changes 用它来给每个文件变更贴上正确标签。

format_file_change_diff302–317 ↗
fn format_file_change_diff(change: &FileChange) -> String

作用:把一个文件改动整理成适合显示的文本内容。新增和删除直接显示内容,更新则显示 diff,也就是对比前后差异的文本。

数据流:输入是 FileChange。新增文件时输出新增内容;删除文件时输出被删内容;更新文件时输出 unified_diff,如果这个更新还包含移动文件,就在 diff 后面追加一行“MOVED to”说明目标路径。输出是一个字符串,给客户端显示文件变化细节。

调用关系:它也是 convert_patch_changes 的内部帮手,负责“改动内容怎么写出来”。当文件移动时它会调用格式化字符串的能力,把移动目标补到 diff 后面,避免用户只看到内容变化却不知道文件被挪了地方。

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

app-server/src/request_processors/token_usage_replay.rs源码 ↗
domain_logicclient attach / request handling

这里解决的是一个重连后的补课问题。token 可以粗略理解成模型读写文字时消耗的“小单位”,系统会保存每轮对话的 token 用量。客户端后来重新接入某个已有线程时,需要知道最近一次用量属于哪一轮对话。这个文件会从已保存的历史记录里找出最后一次 TokenCount,当时正在进行的是哪一轮;如果历史里的轮次 id 还能对上,就用原 id;如果重建线程时 id 变了,就退而求其次,用当时的位置去找现在对应的轮次。最后它只把这条补发通知发给刚接入的那个连接,而不是广播给所有订阅者,避免别人正在看实时消息时突然收到一条旧统计。

函数细节6
send_thread_token_usage_update_to_connection36–58 ↗
async fn send_thread_token_usage_update_to_connection(
    outgoing: &Arc<OutgoingMessageSender>,
    connection_id: ConnectionId,
    thread_id: ThreadId,
    thread: &Thread,
    conversation: &Code

作用:把某个已有线程里保存过的 token 用量,补发给刚连接进来的那个客户端。它的重点是“只发给这个连接”,避免把历史统计误广播给其他正在看实时内容的人。

数据流:输入是发消息的通道、目标连接 id、线程 id、当前重建好的线程、底层对话对象,以及可能已经算好的用量所属轮次 id。它先向对话对象读取 token_usage_info;如果没有保存过用量,就直接不做事。如果有,就把线程 id 转成字符串,把用量转换成对外协议里的 ThreadTokenUsage,再包成 ThreadTokenUsageUpdatedNotification。最后通过 outgoing 把这个服务器通知发给指定连接,外部看到的结果就是该客户端收到一条补发的用量更新。

调用关系:它处在“客户端接入已有线程”的收尾位置:前面的流程决定是否允许补发、并准备好线程信息;这个函数负责真正组装通知并发出去。它会用 token_usage_info 读取保存的用量,用 ThreadTokenUsage::from 做协议格式转换,用 ThreadTokenUsageUpdatedNotification 形成客户端能懂的消息;如果调用方没有给出轮次 id,它会把选择轮次的工作交给 latest_token_usage_turn_id 做兜底。

调用图:调用 2 个内部函数(from, token_usage_info);外部调用 2 个(ThreadTokenUsageUpdated, to_string)。

latest_token_usage_turn_id_from_rollout_items69–98 ↗
fn latest_token_usage_turn_id_from_rollout_items(
    rollout_items: &[RolloutItem],
    turns: &[Turn],
) -> Option<String>

作用:从一串历史记录里判断“最近一次 token 用量统计应该归到哪一轮对话”。这能让重连后的界面把用量显示在正确的回合上,而不是随便挂到最后一轮。

数据流:输入是保存下来的 rollout_items 历史记录,以及已经重建出来的 turns 轮次列表。它一边用 ThreadHistoryBuilder 重新走一遍历史,一边盯着 TokenCount 事件;每次遇到 TokenCount,就记下当时活跃轮次的 id 和位置。遍历结束后,它拿最后一次记录到的归属者来对照当前 turns:如果原 id 还存在,就返回这个 id;如果原 id 对不上,就用当时的位置去当前 turns 里取对应轮次的 id;如果这些信息都没有,就返回空。

调用关系:它是正常情况下最可靠的归属判断器,通常会在重放 token 用量前被上层流程使用。它通过 ThreadHistoryBuilder::new 建一个“历史复盘器”,用 builder 按顺序消化每条 rollout item;matches! 用来识别 TokenCount 这类用量统计事件。它不负责发通知,只负责告诉后续发送逻辑:这条用量应该挂在哪个 turn id 上。

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

latest_token_usage_turn_id105–114 ↗
fn latest_token_usage_turn_id(thread: &Thread) -> String

作用:在没法从历史记录准确判断归属时,选一个尽量合理的轮次 id 作为兜底。它保证补发通知的格式稳定,不至于因为缺少归属信息就发不出去。

数据流:输入是当前线程对象。它从后往前找最后一个已经完成或失败的轮次;如果找不到,就用线程里的最后一轮;如果线程连一轮都没有,就返回空字符串。输出是一个字符串形式的 turn id,不会修改任何数据。

调用关系:它是 send_thread_token_usage_update_to_connection 的备选方案:当调用方没有提前算好 token_usage_turn_id 时,就由它临时挑一个最可能合适的轮次。正常流程更偏向使用 latest_token_usage_turn_id_from_rollout_items,因为那个函数能根据历史位置判断得更准;这个函数负责异常情况下的保底。

tests::replay_attribution_uses_already_loaded_history126–134 ↗
fn replay_attribution_uses_already_loaded_history()

作用:这个测试确认:如果历史记录里的轮次 id 和重建后的轮次 id 能对上,token 用量就应该归到原来的那一轮。

数据流:它先用 token_usage_history 造出一段包含用户消息、助手回复、TokenCount 和下一轮用户消息的假历史。然后用 build_turns_from_rollout_items 把历史重建成轮次列表。接着调用 latest_token_usage_turn_id_from_rollout_items,最后用 assert_eq! 检查返回值是不是第一轮的 id。

调用关系:它是对 latest_token_usage_turn_id_from_rollout_items 的基础场景验证。token_usage_history 提供固定样本,build_turns_from_rollout_items 模拟真实的线程重建过程,assert_eq! 负责确认归属判断没有偏到后面的轮次。

调用图:外部调用 3 个(token_usage_history, assert_eq!, build_turns_from_rollout_items)。

tests::replay_attribution_falls_back_to_rebuilt_turn_position137–146 ↗
fn replay_attribution_falls_back_to_rebuilt_turn_position()

作用:这个测试确认:如果重建后轮次 id 变了,系统仍然能靠“当时是第几轮”把 token 用量归到正确轮次。

数据流:它先生成同样的假历史,并重建出轮次列表。然后故意把第一轮的 id 改成 rebuilt-turn-id,模拟历史 id 和现在线程 id 对不上的情况。接着调用 latest_token_usage_turn_id_from_rollout_items,最后检查结果是不是改过后的第一轮 id。

调用关系:它验证的是 latest_token_usage_turn_id_from_rollout_items 的容错能力。这个测试说明该函数不是死认旧 id,而是在 id 对不上时会使用保存下来的轮次位置来找当前对应的轮次,这正是文件开头注释里强调的重要行为。

调用图:外部调用 3 个(token_usage_history, assert_eq!, build_turns_from_rollout_items)。

tests::token_usage_history148–176 ↗
fn token_usage_history() -> Vec<RolloutItem>

作用:给测试准备一段很小但有代表性的对话历史。里面故意把 TokenCount 放在第一轮回答之后、第二轮开始之前,方便检查用量到底会归到哪一轮。

数据流:它不需要输入。它用 vec! 组装四条历史项:第一条用户消息、第一条助手消息、一条 TokenCount 用量统计、第二条用户消息。输出是一组 RolloutItem,供测试函数拿去重建线程和验证归属。

调用关系:它是两个测试共用的样本制造器。replay_attribution_uses_already_loaded_history 和 replay_attribution_falls_back_to_rebuilt_turn_position 都先调用它拿到同一段历史,这样两个测试只改变“轮次 id 是否对得上”这个条件,便于准确验证归属逻辑。

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

app-server-protocol/src/protocol/thread_history.rs源码 ↗
domain_logicrollout replay / live event handling / session resume

这个文件像一个“聊天记录整理员”。系统运行时会不断产生事件,比如用户发消息、模型回复、执行命令、搜索网页、改文件、调用工具、开启或结束一轮对话。ThreadHistoryBuilder 会按顺序吃进这些事件,把它们归到一个个 Turn(一次对话回合)里,并生成 ThreadItem(回合里的具体条目)。它还处理麻烦情况:有些旧日志没有明确回合边界,有些工具结果会晚到,有些回合会被回滚删除,有些内容会流式追加。文件还提供“增量变更”能力,只告诉外部哪些条目或回合变了,避免每次重读整份历史。测试覆盖了大量边角情况,保证恢复历史时不会把内容放错回合。

函数细节127
build_turns_from_rollout_items78–84 ↗
fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn>

作用:把保存下来的 rollout 条目重新拼成完整的对话回合列表。常用于恢复历史记录或测试历史重建是否正确。

数据流:输入是一串 RolloutItem → 新建一个 ThreadHistoryBuilder,逐条喂给它 → 输出整理好的 Vec<Turn>,每个 Turn 都包含这一轮的可展示内容和状态。

调用关系:它是最外层的便捷入口,内部把实际工作交给 ThreadHistoryBuilder::handle_rollout_item,最后用 ThreadHistoryBuilder::finish 收尾;大量测试都通过它验证重放历史的结果。

调用图:调用 1 个内部函数(new);被 29 处调用(assigns_late_exec_completion_to_original_turn, drops_last_turns_on_thread_rollback, error_then_turn_complete_preserves_failed_status, ignores_plain_user_response_items_in_rollout_replay, ignores_user_message_item_lifecycle_events, late_turn_aborted_does_not_interrupt_active_turn, late_turn_complete_does_not_close_active_turn, marks_turn_as_interrupted_when_aborted, out_of_turn_error_does_not_create_or_fail_a_turn, preserves_agent_message_phase_in_history (+15 more))。

ThreadHistoryChangeSet::is_empty114–118 ↗
fn is_empty(&self) -> bool

作用:判断一次处理后到底有没有任何变化。外部可以用它决定要不要通知界面刷新。

数据流:读取 changed_items、changed_turns、removed_turn_ids 三个列表 → 看它们是否都为空 → 返回 true 或 false,不修改数据。

调用关系:它服务于增量更新流程,是 ThreadHistoryChangeSet 的小工具;谁拿到变更集后都可以用它快速跳过空更新。

ThreadHistoryTurnChange::from_pending_turn122–131 ↗
fn from_pending_turn(turn: &PendingTurn) -> Self

作用:把还没正式收尾的 PendingTurn,摘出一份轻量的回合状态快照。

数据流:输入一个 PendingTurn 引用 → 复制它的 id、状态、错误和时间信息 → 输出 ThreadHistoryTurnChange。

调用关系:record_changed_pending_turn 会用它,把正在进行的回合变化记进增量变更集。

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

ThreadHistoryTurnChange::from_turn133–142 ↗
fn from_turn(turn: &Turn) -> Self

作用:把已经完成保存的 Turn,转成一份轻量状态快照。

数据流:输入一个 Turn 引用 → 复制回合 id、状态、错误和时间字段 → 输出 ThreadHistoryTurnChange。

调用关系:当已结束的回合后来又收到完成或中断事件时,handle_turn_aborted 和 handle_turn_complete 会用它记录变化。

调用图:被 2 处调用(handle_turn_aborted, handle_turn_complete)。

ThreadHistoryChangeAccumulator::push159–169 ↗
fn push(&mut self, changes: ThreadHistoryChangeSet)

作用:把一次又一次的小变更合并进批量变更收集器里。

数据流:输入一个 ThreadHistoryChangeSet → 先处理被删除的回合,再处理条目变化和回合变化 → 更新内部累计列表。

调用关系:handle_rollout_items_with_changes 在批量处理 rollout 条目时反复调用它;它把细活分给 push_removed_turn_id、push_item_change、push_turn_change。

调用图:调用 3 个内部函数(push_item_change, push_removed_turn_id, push_turn_change)。

ThreadHistoryChangeAccumulator::finish171–177 ↗
fn finish(self) -> ThreadHistoryChangeSet

作用:结束累计,把内部临时结构变成最终能发给外部的变更集。

数据流:读取内部列表,丢掉已经被标空的旧变化 → 输出 ThreadHistoryChangeSet。

调用关系:handle_rollout_items_with_changes 在所有条目处理完后调用它,拿到去重后的最终结果。

ThreadHistoryChangeAccumulator::push_item_change179–189 ↗
fn push_item_change(&mut self, change: ThreadHistoryItemChange)

作用:记录某个回合里某个条目的最新样子;如果同一条目重复变化,只保留最后一次。

数据流:输入 ThreadHistoryItemChange → 用“回合 id + 条目 id”查旧位置 → 有旧的就覆盖,没有就追加。

调用关系:只由 ThreadHistoryChangeAccumulator::push 调用,负责批量增量更新里的条目去重。

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

ThreadHistoryChangeAccumulator::push_turn_change191–200 ↗
fn push_turn_change(&mut self, change: ThreadHistoryTurnChange)

作用:记录某个回合状态的最新样子;同一回合多次变化时只保留最后一次。

数据流:输入 ThreadHistoryTurnChange → 按 turn_id 查旧位置 → 覆盖或追加内部列表。

调用关系:只由 ThreadHistoryChangeAccumulator::push 调用,负责把回合开始、完成、失败等状态压缩成最终快照。

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

ThreadHistoryChangeAccumulator::push_removed_turn_id202–224 ↗
fn push_removed_turn_id(&mut self, turn_id: String)

作用:记录某个回合被回滚删除,并清掉之前为这个回合累计的无效变化。

数据流:输入 turn_id → 加入删除列表并去重 → 移除这个回合相关的条目变化和回合变化。

调用关系:ThreadHistoryChangeAccumulator::push 遇到删除回合时调用它,保证外部不会先收到“新增回合”又收到同一回合的过期内容。

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

ThreadHistoryBuilder::default237–239 ↗
fn default() -> Self

作用:提供默认创建方式,等同于新建一个空的历史构建器。

数据流:没有输入 → 调用 ThreadHistoryBuilder::new → 得到初始状态的 builder。

调用关系:让这个类型能用 Rust 的 Default 习惯创建;实际初始化逻辑集中在 new。

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

ThreadHistoryBuilder::new243–252 ↗
fn new() -> Self

作用:创建一个空的历史构建器,准备从第一条事件开始整理对话历史。

数据流:没有输入 → 初始化空回合列表、空当前回合、条目编号和 rollout 位置 → 输出 ThreadHistoryBuilder。

调用关系:build_turns_from_rollout_items 和很多测试都会先调用它;后续所有 handle_* 方法都在这个状态上工作。

调用图:被 15 处调用(build_turns_from_rollout_items, apply_patch_approval_request_updates_active_turn_snapshot_with_file_change, builds_multiple_turns_with_reasoning_items, changed_rollout_item_reports_new_item_snapshot, changed_rollout_item_reports_streaming_item_mutation, changed_rollout_item_reports_turn_completion_metadata, changed_rollout_item_reports_updated_existing_item_snapshot, changed_rollout_items_dedupe_turn_metadata_snapshots, changed_rollout_items_dedupe_updated_item_snapshots, changed_rollout_items_drop_prior_changes_for_removed_turns (+5 more));外部调用 1 个(new)。

ThreadHistoryBuilder::reset254–256 ↗
fn reset(&mut self)

作用:把构建器清空,回到刚创建时的状态。

数据流:输入当前 builder → 用新的空 builder 覆盖自己 → 之前累积的历史和当前回合都被丢弃。

调用关系:clear_listener、track_current_turn_event 等外部流程在需要重新跟踪历史时会调用它。

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

ThreadHistoryBuilder::finish258–261 ↗
fn finish(mut self) -> Vec<Turn>

作用:结束历史构建,拿到最终的回合列表。

数据流:输入 builder 自身 → 先把还开着的当前回合收尾 → 输出 Vec<Turn>。

调用关系:build_turns_from_rollout_items 最后会调用它;内部依赖 finish_current_turn 避免漏掉最后一轮。

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

ThreadHistoryBuilder::active_turn_snapshot263–268 ↗
fn active_turn_snapshot(&self) -> Option<Turn>

作用:拿到当前活跃回合的快照;如果没有活跃回合,就拿最后一个已完成回合。

数据流:读取 current_turn 和 turns → 克隆出一个 Turn → 返回 Option<Turn>,不改变内部状态。

调用关系:外部想显示“当前这一轮”的即时状态时会用它,测试也用它确认文件修改等事件已进入当前回合。

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

ThreadHistoryBuilder::turn_snapshot270–276 ↗
fn turn_snapshot(&self, turn_id: &str) -> Option<Turn>

作用:按回合 id 查询某一轮的完整快照。

数据流:输入 turn_id → 先看当前回合,再看已完成列表 → 找到就克隆 Turn 返回,否则返回 None。

调用关系:这是给外部按需读取单个回合的查询接口,不参与事件写入流程。

ThreadHistoryBuilder::active_turn_position282–290 ↗
fn active_turn_position(&self) -> Option<usize>

作用:告诉外部当前活跃回合在最终列表里会排第几个。

数据流:读取 current_turn 和已完成 turns 的长度 → 算出索引位置 → 返回 Option<usize>。

调用关系:用于界面或投影器定位活跃回合;它只看状态,不创建或修改回合。

ThreadHistoryBuilder::has_active_turn292–294 ↗
fn has_active_turn(&self) -> bool

作用:判断现在是否有一个正在打开的回合。

数据流:读取 current_turn 是否为 Some → 返回布尔值。

调用关系:track_current_turn_event 会用它决定当前是否已经在跟踪一轮对话。

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

ThreadHistoryBuilder::active_turn_id_if_explicit296–301 ↗
fn active_turn_id_if_explicit(&self) -> Option<String>

作用:如果当前回合是由明确的 turn_started 打开的,就返回它的 id。

数据流:读取 current_turn → 检查 opened_explicitly 标记 → 符合就复制 id 返回,否则返回 None。

调用关系:帮助外部区分新协议里的明确回合和旧日志里临时推断出来的回合。

ThreadHistoryBuilder::active_turn_start_index303–307 ↗
fn active_turn_start_index(&self) -> Option<usize>

作用:返回当前回合从第几个 rollout 条目开始。

数据流:读取 current_turn 的 rollout_start_index → 返回 Option<usize>。

调用关系:用于恢复、重连或定位增量历史时知道这一轮从日志哪里开始。

ThreadHistoryBuilder::handle_event314–378 ↗
fn handle_event(&mut self, event: &EventMsg)

作用:这是事件分发中心:看到不同种类的 EventMsg,就交给对应的小处理函数。

数据流:输入一条 EventMsg → 按类型匹配,比如用户消息、工具开始、工具结束、回合完成等 → 修改 builder 里的回合和条目。

调用关系:handle_rollout_item 和 track_current_turn_event 会调用它;它把具体工作交给 handle_user_message、handle_exec_command_end 等众多专门函数。

调用图:调用 40 个内部函数(handle_agent_message, handle_agent_reasoning, handle_agent_reasoning_raw_content, handle_apply_patch_approval_request, handle_collab_agent_interaction_begin, handle_collab_agent_interaction_end, handle_collab_agent_spawn_begin, handle_collab_agent_spawn_end, handle_collab_close_begin, handle_collab_close_end (+15 more));被 2 处调用(handle_rollout_item, track_current_turn_event)。

ThreadHistoryBuilder::handle_rollout_item380–391 ↗
fn handle_rollout_item(&mut self, item: &RolloutItem)

作用:处理保存日志里的一个条目,并维护当前处理到日志第几个位置。

数据流:输入 RolloutItem → 更新 rollout 索引 → 如果是事件就交给 handle_event,如果是压缩或响应项就走对应处理,否则忽略。

调用关系:build_turns_from_rollout_items 会逐条调用它;它是持久化日志重放进入 builder 的主要入口。

调用图:调用 3 个内部函数(handle_compacted, handle_event, handle_response_item)。

ThreadHistoryBuilder::handle_event_with_changes395–397 ↗
fn handle_event_with_changes(&mut self, event: &EventMsg) -> ThreadHistoryChangeSet

作用:处理一条事件,同时返回这条事件造成了哪些可见变化。

数据流:输入 EventMsg → 开启变更收集 → 调用 handle_event → 输出 ThreadHistoryChangeSet。

调用关系:它把普通事件处理包装进 collect_changes,适合实时同步界面时使用。

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

ThreadHistoryBuilder::handle_rollout_item_with_changes401–406 ↗
fn handle_rollout_item_with_changes(
        &mut self,
        item: &RolloutItem,
    ) -> ThreadHistoryChangeSet

作用:处理一个 rollout 条目,并告诉调用方这个条目让历史哪里变了。

数据流:输入 RolloutItem → 开启变更收集 → 调用 handle_rollout_item → 返回新增、更新或删除的变化。

调用关系:handle_rollout_items_with_changes 会反复调用它,单条增量处理也可以直接用它。

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

ThreadHistoryBuilder::handle_rollout_items_with_changes411–420 ↗
fn handle_rollout_items_with_changes(
        &mut self,
        items: &[RolloutItem],
    ) -> ThreadHistoryChangeSet

作用:批量处理多条 rollout 条目,并把重复变化合并成最后结果。

数据流:输入 RolloutItem 切片 → 每条都调用 handle_rollout_item_with_changes → 用 ThreadHistoryChangeAccumulator 去重 → 输出最终变更集。

调用关系:用于一次追加多条日志时,避免外部收到同一条目多次中间状态。

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

ThreadHistoryBuilder::collect_changes422–427 ↗
fn collect_changes(&mut self, handle: impl FnOnce(&mut Self)) -> ThreadHistoryChangeSet

作用:临时打开“记录变化”模式,运行一段处理逻辑后收集结果。

数据流:输入一个闭包 → 放入空的 active_change_set → 执行闭包 → 取出并返回收集到的 ThreadHistoryChangeSet。

调用关系:handle_event_with_changes 和 handle_rollout_item_with_changes 都靠它实现增量变化收集。

调用图:被 2 处调用(handle_event_with_changes, handle_rollout_item_with_changes);外部调用 2 个(default, debug_assert!)。

ThreadHistoryBuilder::handle_response_item429–453 ↗
fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem)

作用:从模型响应项里识别特殊的 hook 提示,并把它放进当前回合。

数据流:输入 ResponseItem → 只接受用户角色的消息 → 尝试解析 hook prompt → 成功后生成 ThreadItem::HookPrompt 并加入当前回合。

调用关系:handle_rollout_item 遇到 RolloutItem::ResponseItem 时调用它;它依赖 parse_hook_prompt_message 识别特殊格式。

调用图:调用 2 个内部函数(push_item_in_current_turn, parse_hook_prompt_message);被 1 处调用(handle_rollout_item)。

ThreadHistoryBuilder::handle_user_message455–472 ↗
fn handle_user_message(&mut self, payload: &UserMessageEvent)

作用:把用户输入变成历史里的用户消息条目。

数据流:输入 UserMessageEvent → 必要时结束旧的隐式回合 → 生成新 item id,整理文字和图片 → 推入当前回合。

调用关系:handle_event 收到 UserMessage 时调用它;它会用 build_user_inputs、next_item_id 和 push_item_in_current_turn。

调用图:调用 4 个内部函数(build_user_inputs, finish_current_turn, next_item_id, push_item_in_current_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_agent_message474–491 ↗
fn handle_agent_message(
        &mut self,
        text: String,
        phase: Option<MessagePhase>,
        memory_citation: Option<crate::protocol::v2::MemoryCitation>,
    )

作用:把模型给用户看的回复加入当前回合。

数据流:输入文本、阶段和记忆引用 → 空文本直接忽略 → 分配 item id → 生成 AgentMessage 条目放入当前回合。

调用关系:handle_event 收到 AgentMessage 时调用它;它把新增条目的实际写入交给 push_item_in_current_turn。

调用图:调用 2 个内部函数(next_item_id, push_item_in_current_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_agent_reasoning493–531 ↗
fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent)

作用:记录模型的“思考摘要”,并尽量合并到上一条思考条目里。

数据流:输入 AgentReasoningEvent → 空文本忽略 → 如果最后一条已经是 Reasoning,就追加 summary;否则新建 Reasoning 条目。

调用关系:handle_event 调用它;在增量模式下,它会用 record_changed_item 报告被追加的思考条目变化。

调用图:调用 5 个内部函数(ensure_turn, is_tracking_changes, next_item_id, push_item_in_current_turn, record_changed_item);被 1 处调用(handle_event);外部调用 2 个(new, vec!)。

ThreadHistoryBuilder::handle_agent_reasoning_raw_content533–571 ↗
fn handle_agent_reasoning_raw_content(&mut self, payload: &AgentReasoningRawContentEvent)

作用:记录模型的原始思考内容,并合并到上一条思考条目。

数据流:输入 AgentReasoningRawContentEvent → 空文本忽略 → 最后一条是 Reasoning 就追加 content,否则新建 Reasoning 条目。

调用关系:handle_event 调用它;它和 handle_agent_reasoning 配合,把摘要和原文放在同一个思考条目中。

调用图:调用 5 个内部函数(ensure_turn, is_tracking_changes, next_item_id, push_item_in_current_turn, record_changed_item);被 1 处调用(handle_event);外部调用 2 个(new, vec!)。

ThreadHistoryBuilder::handle_item_started573–601 ↗
fn handle_item_started(&mut self, payload: &ItemStartedEvent)

作用:处理某些“条目开始了”的生命周期事件,目前主要恢复计划和睡眠条目。

数据流:输入 ItemStartedEvent → 如果是非空 Plan 或 Sleep,就转成 ThreadItem → 按 turn_id 插入或更新对应回合。

调用关系:handle_event 收到 ItemStarted 时调用它;多数其他条目类型已有更专门的事件处理,所以这里会忽略。

调用图:调用 2 个内部函数(upsert_item_in_turn_id, from);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_item_completed603–631 ↗
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent)

作用:处理某些“条目完成了”的生命周期事件,用完成态内容更新历史。

数据流:输入 ItemCompletedEvent → 对非空 Plan 或 Sleep 转成 ThreadItem → 按 turn_id upsert 到对应回合。

调用关系:handle_event 收到 ItemCompleted 时调用它;它和 handle_item_started 保持同类条目在历史中可恢复。

调用图:调用 2 个内部函数(upsert_item_in_turn_id, from);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_web_search_begin633–640 ↗
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent)

作用:在历史里放入一个正在进行的网页搜索占位条目。

数据流:输入 WebSearchBeginEvent → 用 call_id 做条目 id,query 先为空 → 插入或更新当前回合。

调用关系:handle_event 收到 WebSearchBegin 时调用;后续 handle_web_search_end 会用同一 id 更新成完整结果。

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

ThreadHistoryBuilder::handle_web_search_end642–649 ↗
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent)

作用:把网页搜索的最终查询和动作写入历史。

数据流:输入 WebSearchEndEvent → 生成带 query 和 action 的 WebSearch 条目 → 用 call_id 更新当前回合里的搜索条目。

调用关系:handle_event 收到 WebSearchEnd 时调用;它通常覆盖 handle_web_search_begin 先放进去的占位条目。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, from);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_exec_command_begin651–654 ↗
fn handle_exec_command_begin(&mut self, payload: &ExecCommandBeginEvent)

作用:记录一条命令开始执行,让历史里能看到工具正在跑。

数据流:输入 ExecCommandBeginEvent → 用构建函数转成命令执行条目 → 按事件里的 turn_id 放进对应回合。

调用关系:handle_event 调用它;具体字段转换交给 build_command_execution_begin_item,放置交给 upsert_item_in_turn_id。

调用图:调用 2 个内部函数(build_command_execution_begin_item, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_exec_command_end656–664 ↗
fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent)

作用:记录命令执行结束,包括输出、退出码和耗时。

数据流:输入 ExecCommandEndEvent → 转成完整的命令执行条目 → 按 turn_id 更新原来的回合,而不是盲目放进当前回合。

调用关系:handle_event 调用它;这样即使命令结果晚到,也会回到原始回合,不会串到新用户回合里。

调用图:调用 2 个内部函数(build_command_execution_end_item, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_guardian_assessment666–683 ↗
fn handle_guardian_assessment(&mut self, payload: &GuardianAssessmentEvent)

作用:把安全审查事件转成用户能看懂的命令或操作状态。

数据流:输入 GuardianAssessmentEvent → 把审查状态映射成执行状态 → 构建 ThreadItem → 按 turn_id 或当前回合插入更新。

调用关系:handle_event 调用它;Approved 审查不会生成条目,其他状态交给 build_item_from_guardian_event 转换。

调用图:调用 3 个内部函数(build_item_from_guardian_event, upsert_item_in_current_turn, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_apply_patch_approval_request685–692 ↗
fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent)

作用:记录一次文件修改需要审批的请求。

数据流:输入 ApplyPatchApprovalRequestEvent → 构造成 FileChange 条目 → 有 turn_id 就放到指定回合,否则放当前回合。

调用关系:handle_event 调用它;后续 patch begin/end 可能用同一 id 更新文件变更状态。

调用图:调用 3 个内部函数(build_file_change_approval_request_item, upsert_item_in_current_turn, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_patch_apply_begin694–701 ↗
fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent)

作用:记录文件补丁开始应用,显示文件改动正在进行。

数据流:输入 PatchApplyBeginEvent → 转成进行中的 FileChange 条目 → 按 turn_id 或当前回合插入更新。

调用关系:handle_event 调用它;转换细节由 build_file_change_begin_item 处理。

调用图:调用 3 个内部函数(build_file_change_begin_item, upsert_item_in_current_turn, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_patch_apply_end703–710 ↗
fn handle_patch_apply_end(&mut self, payload: &PatchApplyEndEvent)

作用:记录文件补丁应用结束,显示成功、失败或被拒绝后的文件变更结果。

数据流:输入 PatchApplyEndEvent → 转成最终 FileChange 条目 → 更新指定回合或当前回合。

调用关系:handle_event 调用它;它和 handle_patch_apply_begin 使用同一 upsert 机制,保证同一个补丁条目被更新而不是重复出现。

调用图:调用 3 个内部函数(build_file_change_end_item, upsert_item_in_current_turn, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_dynamic_tool_call_request712–731 ↗
fn handle_dynamic_tool_call_request(
        &mut self,
        payload: &codex_protocol::dynamic_tools::DynamicToolCallRequest,
    )

作用:记录一次动态工具调用刚发起,状态为进行中。

数据流:输入 DynamicToolCallRequest → 取调用 id、命名空间、工具名和参数 → 生成 InProgress 的 DynamicToolCall 条目。

调用关系:handle_event 调用它;后续 handle_dynamic_tool_call_response 会用同一 call_id 更新结果。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, upsert_item_in_turn_id);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_dynamic_tool_call_response733–755 ↗
fn handle_dynamic_tool_call_response(&mut self, payload: &DynamicToolCallResponseEvent)

作用:记录动态工具调用的返回结果和耗时。

数据流:输入 DynamicToolCallResponseEvent → 根据 success 判定完成或失败,转换内容项和耗时 → 更新对应 DynamicToolCall 条目。

调用关系:handle_event 调用它;内容转换交给 convert_dynamic_tool_content_items,放置交给 upsert 方法。

调用图:调用 3 个内部函数(upsert_item_in_current_turn, upsert_item_in_turn_id, convert_dynamic_tool_content_items);被 1 处调用(handle_event);外部调用 1 个(try_from)。

ThreadHistoryBuilder::handle_mcp_tool_call_begin757–775 ↗
fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent)

作用:记录 MCP 工具调用开始。MCP 是一种让模型调用外部工具的协议。

数据流:输入 McpToolCallBeginEvent → 取服务器、工具名、参数等 → 创建 InProgress 的 McpToolCall 条目放入当前回合。

调用关系:handle_event 调用它;结束事件会由 handle_mcp_tool_call_end 更新这个条目。

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

ThreadHistoryBuilder::handle_mcp_tool_call_end777–817 ↗
fn handle_mcp_tool_call_end(&mut self, payload: &McpToolCallEndEvent)

作用:记录 MCP 工具调用结束,并保存结果或错误。

数据流:输入 McpToolCallEndEvent → 判断成功失败,换算耗时,拆出 result 或 error → 更新当前回合里的 McpToolCall 条目。

调用关系:handle_event 调用它;它用 call_id 覆盖开始时的占位状态。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, is_success);被 1 处调用(handle_event);外部调用 2 个(new, try_from)。

ThreadHistoryBuilder::handle_view_image_tool_call819–825 ↗
fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent)

作用:记录模型查看了一张本地或远程图片。

数据流:输入 ViewImageToolCallEvent → 用 call_id 和路径创建 ImageView 条目 → 插入或更新当前回合。

调用关系:handle_event 调用它;这是图片查看工具在历史里的展示入口。

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

ThreadHistoryBuilder::handle_image_generation_begin827–836 ↗
fn handle_image_generation_begin(&mut self, payload: &ImageGenerationBeginEvent)

作用:记录图片生成开始,先放一个空结果的占位条目。

数据流:输入 ImageGenerationBeginEvent → 用 call_id 创建 ImageGeneration 条目,状态和结果暂为空 → 放进当前回合。

调用关系:handle_event 调用它;handle_image_generation_end 后续会补上结果。

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

ThreadHistoryBuilder::handle_image_generation_end838–847 ↗
fn handle_image_generation_end(&mut self, payload: &ImageGenerationEndEvent)

作用:记录图片生成完成后的状态、结果和保存路径。

数据流:输入 ImageGenerationEndEvent → 复制状态、修订提示词、结果和文件路径 → 更新当前回合的图片生成条目。

调用关系:handle_event 调用它;它通常覆盖 begin 阶段的占位条目。

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

ThreadHistoryBuilder::handle_collab_agent_spawn_begin849–865 ↗
fn handle_collab_agent_spawn_begin(
        &mut self,
        payload: &codex_protocol::protocol::CollabAgentSpawnBeginEvent,
    )

作用:记录开始创建一个协作子代理。子代理可以理解成被主对话派出去干活的助手。

数据流:输入 CollabAgentSpawnBeginEvent → 保存发送方、提示词、模型和推理力度 → 创建进行中的协作工具条目。

调用关系:handle_event 调用它;spawn end 会用同一 call_id 更新创建结果和子代理状态。

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

ThreadHistoryBuilder::handle_collab_agent_spawn_end867–899 ↗
fn handle_collab_agent_spawn_end(
        &mut self,
        payload: &codex_protocol::protocol::CollabAgentSpawnEndEvent,
    )

作用:记录创建协作子代理的最终结果。

数据流:输入 CollabAgentSpawnEndEvent → 看是否拿到新线程 id 和状态 → 生成完成或失败的 CollabAgentToolCall 条目。

调用关系:handle_event 调用它;它把 AgentStatus 转成 CollabAgentState,展示新子代理是否运行、失败或不存在。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, from);被 1 处调用(handle_event);外部调用 3 个(new, new, vec!)。

ThreadHistoryBuilder::handle_collab_agent_interaction_begin901–917 ↗
fn handle_collab_agent_interaction_begin(
        &mut self,
        payload: &codex_protocol::protocol::CollabAgentInteractionBeginEvent,
    )

作用:记录开始给某个协作代理发送输入。

数据流:输入 CollabAgentInteractionBeginEvent → 保存发送方、接收方和 prompt → 创建进行中的 SendInput 条目。

调用关系:handle_event 调用它;interaction end 会补上接收方代理的最终状态。

调用图:调用 1 个内部函数(upsert_item_in_current_turn);被 1 处调用(handle_event);外部调用 2 个(new, vec!)。

ThreadHistoryBuilder::handle_collab_agent_interaction_end919–940 ↗
fn handle_collab_agent_interaction_end(
        &mut self,
        payload: &codex_protocol::protocol::CollabAgentInteractionEndEvent,
    )

作用:记录给协作代理发送输入后的结果。

数据流:输入 CollabAgentInteractionEndEvent → 根据状态判断工具调用成功或失败 → 写入接收方代理状态和 prompt。

调用关系:handle_event 调用它;它把协作代理反馈转成前端可展示的 CollabAgentToolCall。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, from);被 1 处调用(handle_event);外部调用 1 个(vec!)。

ThreadHistoryBuilder::handle_sub_agent_activity942–952 ↗
fn handle_sub_agent_activity(
        &mut self,
        payload: &codex_protocol::protocol::SubAgentActivityEvent,
    )

作用:记录子代理发生的一条活动,比如某个代理有了动作或状态变化。

数据流:输入 SubAgentActivityEvent → 取事件 id、活动类型、代理线程 id 和路径 → 生成 SubAgentActivity 条目。

调用关系:handle_event 调用它;它把子代理自身活动加入主回合历史。

调用图:调用 1 个内部函数(upsert_item_in_current_turn);被 1 处调用(handle_event);外部调用 1 个(from)。

ThreadHistoryBuilder::handle_collab_waiting_begin954–974 ↗
fn handle_collab_waiting_begin(
        &mut self,
        payload: &codex_protocol::protocol::CollabWaitingBeginEvent,
    )

作用:记录主代理开始等待一个或多个协作代理。

数据流:输入 CollabWaitingBeginEvent → 收集接收方线程 id → 创建进行中的 Wait 协作工具条目。

调用关系:handle_event 调用它;waiting end 会更新等待结果和各代理状态。

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

ThreadHistoryBuilder::handle_collab_waiting_end976–1008 ↗
fn handle_collab_waiting_end(
        &mut self,
        payload: &codex_protocol::protocol::CollabWaitingEndEvent,
    )

作用:记录等待协作代理结束,并整理每个代理的状态。

数据流:输入 CollabWaitingEndEvent → 如果任何代理错误或找不到则标为失败,否则完成 → 排序接收方 id 并保存状态表。

调用关系:handle_event 调用它;它更新 begin 阶段创建的 Wait 条目。

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

ThreadHistoryBuilder::handle_collab_close_begin1010–1026 ↗
fn handle_collab_close_begin(
        &mut self,
        payload: &codex_protocol::protocol::CollabCloseBeginEvent,
    )

作用:记录开始关闭一个协作代理。

数据流:输入 CollabCloseBeginEvent → 保存发送方和要关闭的接收方 → 创建进行中的 CloseAgent 条目。

调用关系:handle_event 调用它;close end 会写入关闭结果。

调用图:调用 1 个内部函数(upsert_item_in_current_turn);被 1 处调用(handle_event);外部调用 2 个(new, vec!)。

ThreadHistoryBuilder::handle_collab_close_end1028–1051 ↗
fn handle_collab_close_end(&mut self, payload: &codex_protocol::protocol::CollabCloseEndEvent)

作用:记录关闭协作代理后的成功或失败状态。

数据流:输入 CollabCloseEndEvent → 根据 AgentStatus 判断工具调用状态 → 保存接收方代理最终状态。

调用关系:handle_event 调用它;它把关闭操作的结果更新到当前回合。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, from);被 1 处调用(handle_event);外部调用 1 个(vec!)。

ThreadHistoryBuilder::handle_collab_resume_begin1053–1069 ↗
fn handle_collab_resume_begin(
        &mut self,
        payload: &codex_protocol::protocol::CollabResumeBeginEvent,
    )

作用:记录开始恢复一个协作代理。

数据流:输入 CollabResumeBeginEvent → 保存发送方和接收方线程 id → 创建进行中的 ResumeAgent 条目。

调用关系:handle_event 调用它;resume end 会补充恢复结果。

调用图:调用 1 个内部函数(upsert_item_in_current_turn);被 1 处调用(handle_event);外部调用 2 个(new, vec!)。

ThreadHistoryBuilder::handle_collab_resume_end1071–1097 ↗
fn handle_collab_resume_end(
        &mut self,
        payload: &codex_protocol::protocol::CollabResumeEndEvent,
    )

作用:记录恢复协作代理后的结果。

数据流:输入 CollabResumeEndEvent → 根据代理状态判断完成或失败 → 写入接收方代理状态。

调用关系:handle_event 调用它;相关测试确认它能从日志里重建恢复代理的历史条目。

调用图:调用 2 个内部函数(upsert_item_in_current_turn, from);被 1 处调用(handle_event);外部调用 1 个(vec!)。

ThreadHistoryBuilder::handle_context_compacted1099–1102 ↗
fn handle_context_compacted(&mut self, _payload: &ContextCompactedEvent)

作用:记录上下文被压缩这件事。上下文压缩就是把长历史变短,方便模型继续处理。

数据流:输入 ContextCompactedEvent → 分配 item id → 放入 ContextCompaction 条目。

调用关系:handle_event 调用它;如果是持久化的 CompactedItem,则由 handle_compacted 记录保留回合的标记。

调用图:调用 2 个内部函数(next_item_id, push_item_in_current_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_entered_review_mode1104–1111 ↗
fn handle_entered_review_mode(&mut self, payload: &codex_protocol::protocol::ReviewRequest)

作用:记录系统进入代码审查模式,并显示给用户的提示。

数据流:输入 ReviewRequest → 取 user_facing_hint,缺省时用固定文案 → 生成 EnteredReviewMode 条目。

调用关系:handle_event 调用它;它通过 push_item_in_current_turn 把审查入口放进历史。

调用图:调用 2 个内部函数(next_item_id, push_item_in_current_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_exited_review_mode1113–1124 ↗
fn handle_exited_review_mode(
        &mut self,
        payload: &codex_protocol::protocol::ExitedReviewModeEvent,
    )

作用:记录退出审查模式,并展示审查输出或兜底错误文案。

数据流:输入 ExitedReviewModeEvent → 如果有 review_output 就渲染文字,否则用 fallback → 生成 ExitedReviewMode 条目。

调用关系:handle_event 调用它;文本整理由 render_review_output_text 完成。

调用图:调用 2 个内部函数(next_item_id, push_item_in_current_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_error1126–1145 ↗
fn handle_error(&mut self, payload: &ErrorEvent)

作用:把会影响当前回合的错误标到回合状态上。

数据流:输入 ErrorEvent → 先判断这个错误是否影响回合 → 如果当前有回合,就设为 Failed 并保存错误信息。

调用关系:handle_event 调用它;在增量模式下会通过 record_changed_turn 通知外部回合失败。

调用图:调用 3 个内部函数(is_tracking_changes, record_changed_turn, affects_turn_status);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_turn_aborted1147–1177 ↗
fn handle_turn_aborted(&mut self, payload: &TurnAbortedEvent)

作用:处理某一轮被中断的事件。

数据流:输入 TurnAbortedEvent → 优先按 turn_id 找当前或已完成回合 → 标记 Interrupted 并写入结束时间和耗时。

调用关系:handle_event 调用它;如果事件没带 id 或找不到,才退回处理当前回合,避免误伤新回合。

调用图:调用 2 个内部函数(record_changed_turn, from_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_turn_started1179–1188 ↗
fn handle_turn_started(&mut self, payload: &TurnStartedEvent)

作用:处理明确的一轮对话开始。

数据流:输入 TurnStartedEvent → 先结束旧当前回合 → 用给定 turn_id 创建 InProgress 回合并记录开始时间 → 设置为当前回合。

调用关系:handle_event 调用它;它用 new_turn、record_changed_pending_turn 和 finish_current_turn 建立清晰回合边界。

调用图:调用 3 个内部函数(finish_current_turn, new_turn, record_changed_pending_turn);被 1 处调用(handle_event)。

ThreadHistoryBuilder::handle_turn_complete1190–1233 ↗
fn handle_turn_complete(&mut self, payload: &TurnCompleteEvent)

作用:处理一轮对话完成。

数据流:输入 TurnCompleteEvent → 优先匹配当前回合 id 并标完成,然后收尾;若是已完成回合的迟到事件,就只更新那个回合。

调用关系:handle_event 调用它;它特别防止迟到的完成事件关闭错误的当前回合。

调用图:调用 3 个内部函数(finish_current_turn, record_changed_turn, from_turn);被 1 处调用(handle_event);外部调用 1 个(matches!)。

ThreadHistoryBuilder::handle_compacted1240–1242 ↗
fn handle_compacted(&mut self, _payload: &CompactedItem)

作用:记录日志里出现了压缩标记,即使这一轮没有可展示条目也要保留。

数据流:输入 CompactedItem → 确保有当前回合 → 把 saw_compaction 标记设为 true。

调用关系:handle_rollout_item 遇到 RolloutItem::Compacted 时调用它;finish_current_turn 会根据这个标记决定不丢掉空回合。

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

ThreadHistoryBuilder::handle_thread_rollback1244–1268 ↗
fn handle_thread_rollback(&mut self, payload: &ThreadRolledBackEvent)

作用:处理线程回滚,也就是删掉最后若干轮历史。

数据流:输入 ThreadRolledBackEvent → 先收尾当前回合 → 算出要删除的回合 id 并记录 → 截短 turns 列表,重算下一个 item 编号。

调用关系:handle_event 调用它;增量模式下通过 record_removed_turn_ids 告诉外部哪些回合消失了。

调用图:调用 2 个内部函数(finish_current_turn, record_removed_turn_ids);被 1 处调用(handle_event);外部调用 3 个(new, try_from, try_from)。

ThreadHistoryBuilder::finish_current_turn1270–1277 ↗
fn finish_current_turn(&mut self)

作用:把正在构建的回合放进已完成列表。

数据流:读取 current_turn → 如果是无内容、非明确打开、也没有压缩标记的临时回合就丢弃 → 否则转成 Turn 推入 turns。

调用关系:finish、handle_user_message、handle_turn_started、handle_turn_complete 和 rollback 都会用它关闭当前回合。

调用图:被 5 处调用(finish, handle_thread_rollback, handle_turn_complete, handle_turn_started, handle_user_message);外部调用 1 个(from)。

ThreadHistoryBuilder::new_turn1279–1299 ↗
fn new_turn(&mut self, id: Option<String>) -> PendingTurn

作用:创建一个新的待完成回合 PendingTurn。

数据流:输入可选 id → 有 id 就用它,没有就按场景生成 UUID 或 rollout-N → 初始化空条目和默认状态。

调用关系:ensure_turn 在需要隐式回合时调用它,handle_turn_started 在明确回合开始时也调用它。

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

ThreadHistoryBuilder::ensure_turn1301–1313 ↗
fn ensure_turn(&mut self) -> &mut PendingTurn

作用:确保当前一定有一个可写入的回合。

数据流:如果 current_turn 为空 → 创建新 PendingTurn 并记录回合变化 → 返回当前回合的可变引用。

调用关系:很多写入函数都会先调用它,比如 push_item_in_current_turn、upsert_item_in_current_turn 和思考内容处理。

调用图:调用 2 个内部函数(new_turn, record_changed_pending_turn);被 5 处调用(handle_agent_reasoning, handle_agent_reasoning_raw_content, handle_compacted, push_item_in_current_turn, upsert_item_in_current_turn);外部调用 1 个(unreachable!)。

ThreadHistoryBuilder::push_item_in_current_turn1315–1326 ↗
fn push_item_in_current_turn(&mut self, item: ThreadItem)

作用:把一个全新的条目追加到当前回合末尾。

数据流:输入 ThreadItem → 确保有当前回合 → push 到 items 列表 → 如果在收集变化,就记录这个新增条目。

调用关系:用户消息、模型回复、思考、审查模式等顺序追加的内容都通过它进入历史。

调用图:调用 3 个内部函数(ensure_turn, is_tracking_changes, record_changed_item);被 8 处调用(handle_agent_message, handle_agent_reasoning, handle_agent_reasoning_raw_content, handle_context_compacted, handle_entered_review_mode, handle_exited_review_mode, handle_response_item, handle_user_message)。

ThreadHistoryBuilder::upsert_item_in_turn_id1328–1358 ↗
fn upsert_item_in_turn_id(&mut self, turn_id: &str, item: ThreadItem)

作用:按指定回合 id 插入或更新一个条目。

数据流:输入 turn_id 和 ThreadItem → 先找当前回合,再找已完成回合 → 同 id 条目就替换,没有就追加;找不到回合则警告并丢弃。

调用关系:命令执行、文件修改、动态工具等带 turn_id 的事件用它,尤其能处理迟到结果回到原回合。

调用图:调用 3 个内部函数(is_tracking_changes, record_changed_item, upsert_turn_item);被 10 处调用(handle_apply_patch_approval_request, handle_dynamic_tool_call_request, handle_dynamic_tool_call_response, handle_exec_command_begin, handle_exec_command_end, handle_guardian_assessment, handle_item_completed, handle_item_started, handle_patch_apply_begin, handle_patch_apply_end);外部调用 1 个(warn!)。

ThreadHistoryBuilder::upsert_item_in_current_turn1360–1370 ↗
fn upsert_item_in_current_turn(&mut self, item: ThreadItem)

作用:在当前回合里插入或更新一个条目。

数据流:输入 ThreadItem → 确保当前回合存在 → 用 upsert_turn_item 按条目 id 更新或追加 → 如需增量则记录变化。

调用关系:网页搜索、MCP 工具、图片、协作代理等当前回合事件都会走它。

调用图:调用 4 个内部函数(ensure_turn, is_tracking_changes, record_changed_item, upsert_turn_item);被 24 处调用(handle_apply_patch_approval_request, handle_collab_agent_interaction_begin, handle_collab_agent_interaction_end, handle_collab_agent_spawn_begin, handle_collab_agent_spawn_end, handle_collab_close_begin, handle_collab_close_end, handle_collab_resume_begin, handle_collab_resume_end, handle_collab_waiting_begin (+14 more))。

ThreadHistoryBuilder::is_tracking_changes1372–1374 ↗
fn is_tracking_changes(&self) -> bool

作用:判断当前是否正在收集增量变化。

数据流:读取 active_change_set 是否存在 → 返回 true 或 false。

调用关系:多个写入函数调用它,决定是否需要额外记录 changed_items 或 changed_turns。

调用图:被 7 处调用(handle_agent_reasoning, handle_agent_reasoning_raw_content, handle_error, push_item_in_current_turn, record_changed_pending_turn, upsert_item_in_current_turn, upsert_item_in_turn_id)。

ThreadHistoryBuilder::record_changed_item1376–1382 ↗
fn record_changed_item(&mut self, turn_id: String, item: ThreadItem)

作用:把某个条目的最新快照加入当前变更集。

数据流:输入 turn_id 和 ThreadItem → 如果 active_change_set 存在,就追加 ThreadHistoryItemChange → 否则什么也不做。

调用关系:push_item_in_current_turn、upsert_item_in_current_turn、upsert_item_in_turn_id 和流式思考更新会调用它。

调用图:被 5 处调用(handle_agent_reasoning, handle_agent_reasoning_raw_content, push_item_in_current_turn, upsert_item_in_current_turn, upsert_item_in_turn_id)。

ThreadHistoryBuilder::record_changed_pending_turn1384–1388 ↗
fn record_changed_pending_turn(&mut self, turn: &PendingTurn)

作用:把正在进行的回合状态记录成一次变化。

数据流:输入 PendingTurn → 如果正在收集变化,就转成 ThreadHistoryTurnChange → 写入 changed_turns。

调用关系:ensure_turn 创建隐式回合、handle_turn_started 创建明确回合时会用它。

调用图:调用 3 个内部函数(is_tracking_changes, record_changed_turn, from_pending_turn);被 2 处调用(ensure_turn, handle_turn_started)。

ThreadHistoryBuilder::record_changed_turn1390–1394 ↗
fn record_changed_turn(&mut self, turn: ThreadHistoryTurnChange)

作用:把一个回合状态变化加入当前变更集。

数据流:输入 ThreadHistoryTurnChange → 如果 active_change_set 存在,就追加到 changed_turns。

调用关系:错误、中断、完成和 pending turn 记录都通过它向外报告回合元数据变化。

调用图:被 4 处调用(handle_error, handle_turn_aborted, handle_turn_complete, record_changed_pending_turn)。

ThreadHistoryBuilder::record_removed_turn_ids1396–1400 ↗
fn record_removed_turn_ids(&mut self, removed_turn_ids: Vec<String>)

作用:把被删除的回合 id 加入当前变更集。

数据流:输入多个 turn_id → 如果 active_change_set 存在,就追加到 removed_turn_ids。

调用关系:handle_thread_rollback 调用它,把回滚删除的信息交给增量消费者。

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

ThreadHistoryBuilder::next_item_id1402–1406 ↗
fn next_item_id(&mut self) -> String

作用:生成下一个自动条目 id。

数据流:读取 next_item_index → 生成 item-N 字符串 → 计数加一并返回 id。

调用关系:用户消息、模型消息、思考、审查模式和上下文压缩等没有外部 call_id 的条目会用它。

调用图:被 7 处调用(handle_agent_message, handle_agent_reasoning, handle_agent_reasoning_raw_content, handle_context_compacted, handle_entered_review_mode, handle_exited_review_mode, handle_user_message);外部调用 1 个(format!)。

ThreadHistoryBuilder::build_user_inputs1408–1436 ↗
fn build_user_inputs(&self, payload: &UserMessageEvent) -> Vec<UserInput>

作用:把用户事件里的文字和图片整理成统一的输入列表。

数据流:输入 UserMessageEvent → 非空文字变 Text,远程图片变 Image,本地图片变 LocalImage,并带上清晰度 detail → 输出 Vec<UserInput>。

调用关系:handle_user_message 调用它,让用户消息条目能同时展示文本、远程图和本地图。

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

render_review_output_text1441–1448 ↗
fn render_review_output_text(output: &ReviewOutputEvent) -> String

作用:把审查输出转换成要显示的一段文字。

数据流:输入 ReviewOutputEvent → 取 overall_explanation 并去掉首尾空白 → 空则返回兜底失败文案,否则返回解释文本。

调用关系:handle_exited_review_mode 用它生成退出审查模式时的历史内容。

convert_dynamic_tool_content_items1450–1465 ↗
fn convert_dynamic_tool_content_items(
    items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
) -> Vec<DynamicToolCallOutputContentItem>

作用:把核心协议里的动态工具输出内容,转换成 v2 协议要展示的内容格式。

数据流:输入动态工具输出内容列表 → 逐个复制文本或图片 URL → 输出 Vec<DynamicToolCallOutputContentItem>。

调用关系:handle_dynamic_tool_call_response 调用它,把工具返回内容放进 ThreadItem::DynamicToolCall。

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

upsert_turn_item1467–1478 ↗
fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) -> &ThreadItem

作用:在一个回合的条目列表里按 id 更新已有条目,或追加新条目。

数据流:输入可变 items 列表和新 ThreadItem → 查找同 id 条目 → 找到就替换,找不到就 push → 返回最终列表里的条目引用。

调用关系:upsert_item_in_current_turn 和 upsert_item_in_turn_id 共用它,保证 begin/end 这类事件不会产生重复条目。

调用图:被 2 处调用(upsert_item_in_current_turn, upsert_item_in_turn_id)。

PendingTurn::opened_explicitly1499–1502 ↗
fn opened_explicitly(mut self) -> Self

作用:给待完成回合打上“这是明确开始的回合”标记。

数据流:输入 PendingTurn 自身 → 设置 opened_explicitly 为 true → 返回修改后的 PendingTurn。

调用关系:handle_turn_started 创建新回合时链式调用它,之后 finish_current_turn 会根据这个标记保留空回合。

PendingTurn::with_status1504–1507 ↗
fn with_status(mut self, status: TurnStatus) -> Self

作用:给待完成回合设置状态,方便创建时链式配置。

数据流:输入 PendingTurn 和 TurnStatus → 写入 status 字段 → 返回 PendingTurn。

调用关系:handle_turn_started 用它把新回合标成 InProgress。

PendingTurn::with_started_at1509–1512 ↗
fn with_started_at(mut self, started_at: Option<i64>) -> Self

作用:给待完成回合设置开始时间。

数据流:输入 PendingTurn 和可选时间戳 → 写入 started_at → 返回 PendingTurn。

调用关系:handle_turn_started 用它把 TurnStartedEvent 里的时间带进回合元数据。

Turn::from1531–1542 ↗
fn from(value: &PendingTurn) -> Self

作用:把内部临时的 PendingTurn 转成对外使用的 Turn。

数据流:输入 PendingTurn 或它的引用 → 拷贝或移动 id、items、状态、错误和时间 → 输出 items_view 为 Full 的 Turn。

调用关系:finish_current_turn 和 active_turn_snapshot 等地方依赖这个转换,把构建中的回合变成稳定快照。

tests::builds_multiple_turns_with_reasoning_items1591–1697 ↗
fn builds_multiple_turns_with_reasoning_items()

作用:测试旧式事件流能被拆成多轮,并正确合并思考摘要和原始内容。

数据流:构造用户消息、模型消息和思考事件 → 喂给 builder → 断言得到两轮、条目顺序和内容都正确。

调用关系:它直接使用 ThreadHistoryBuilder::new 和 finish,覆盖 handle_user_message、handle_agent_message、handle_agent_reasoning 等核心路径。

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

tests::rebuilds_user_message_image_details_from_legacy_events1700–1738 ↗
fn rebuilds_user_message_image_details_from_legacy_events()

作用:测试从旧用户消息事件中恢复远程图片和本地图片的 detail 信息。

数据流:输入带图片和清晰度的 rollout 事件 → 调用 build_turns_from_rollout_items → 检查 UserMessage content 是否包含三类输入。

调用关系:验证 build_user_inputs 在历史重放中的效果。

调用图:调用 1 个内部函数(build_turns_from_rollout_items);外部调用 3 个(from, assert_eq!, vec!)。

tests::ignores_user_message_item_lifecycle_events1741–1797 ↗
fn ignores_user_message_item_lifecycle_events()

作用:测试用户消息的生命周期事件不会额外生成重复用户消息。

数据流:构造 turn started、UserMessage、ItemStarted(UserMessage)、TurnComplete → 重建历史 → 断言只有真正的用户消息一条。

调用关系:覆盖 handle_item_started 对 UserMessage 类型的忽略逻辑。

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

tests::rebuilds_sleep_item_from_persisted_completion1800–1844 ↗
fn rebuilds_sleep_item_from_persisted_completion()

作用:测试睡眠条目可以从完成事件恢复出来。

数据流:构造包含 Sleep 的 ItemCompleted → 重放 rollout → 检查 Turn 里有 Sleep 条目和持续时间。

调用关系:验证 handle_item_completed 对 Sleep 的 upsert 路径。

调用图:调用 2 个内部函数(build_turns_from_rollout_items, new);外部调用 3 个(Sleep, assert_eq!, vec!)。

tests::preserves_user_message_client_id_from_legacy_event1847–1905 ↗
fn preserves_user_message_client_id_from_legacy_event()

作用:测试旧事件里的 client_id 不会丢失。

数据流:构造带 client_id 的用户消息事件 → 重建历史 → 检查 UserMessage 条目保留 client_id。

调用关系:覆盖 handle_user_message 从 UserMessageEvent 拷贝 client_id 的行为。

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

tests::preserves_agent_message_phase_in_history1908–1930 ↗
fn preserves_agent_message_phase_in_history()

作用:测试模型消息的阶段信息会保存进历史。

数据流:输入带 FinalAnswer phase 的 AgentMessage → 重建历史 → 检查 ThreadItem::AgentMessage 的 phase。

调用关系:覆盖 handle_agent_message 对 MessagePhase 的传递。

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

tests::replays_image_generation_end_events_into_turn_history1933–1997 ↗
fn replays_image_generation_end_events_into_turn_history()

作用:测试图片生成结束事件能恢复成完整图片生成条目。

数据流:构造一轮用户请求和 ImageGenerationEnd → 重放 → 检查状态、提示词、结果和保存路径。

调用关系:覆盖 handle_image_generation_end 的历史重放路径。

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

tests::splits_reasoning_when_interleaved2000–2051 ↗
fn splits_reasoning_when_interleaved()

作用:测试思考内容被普通模型消息隔开时,会分成两个思考条目。

数据流:输入思考、原始思考、模型消息、第二段思考 → 重放 → 检查两段 Reasoning 分别存在。

调用关系:验证 handle_agent_reasoning 只合并相邻的 Reasoning。

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

tests::marks_turn_as_interrupted_when_aborted2054–2144 ↗
fn marks_turn_as_interrupted_when_aborted()

作用:测试收到中断事件后,当前轮会被标记为 Interrupted。

数据流:输入第一轮消息、中断、第二轮消息 → 重建历史 → 检查第一轮状态中断、第二轮正常完成。

调用关系:覆盖 handle_turn_aborted 与旧式隐式回合切分的配合。

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

tests::drops_last_turns_on_thread_rollback2147–2240 ↗
fn drops_last_turns_on_thread_rollback()

作用:测试回滚会删除最后若干轮,并允许后续新轮继续追加。

数据流:构造两轮、回滚一轮、再来第三轮 → 重放 → 检查历史只剩第一轮和新第三轮。

调用关系:覆盖 handle_thread_rollback 的截断和 item 编号重算。

调用图:调用 1 个内部函数(build_turns_from_rollout_items);外部调用 3 个(assert_eq!, assert_ne!, vec!)。

tests::thread_rollback_clears_all_turns_when_num_turns_exceeds_history2243–2280 ↗
fn thread_rollback_clears_all_turns_when_num_turns_exceeds_history()

作用:测试回滚数量超过现有历史时会清空全部回合。

数据流:构造两轮后回滚 99 轮 → 重放 → 断言输出空列表。

调用关系:验证 handle_thread_rollback 对超大 num_turns 的保护。

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

tests::uses_explicit_turn_boundaries_for_mid_turn_steering2283–2345 ↗
fn uses_explicit_turn_boundaries_for_mid_turn_steering()

作用:测试明确回合边界内的多条用户消息会留在同一轮。

数据流:输入 TurnStarted、两条 UserMessage、TurnComplete → 重建 → 检查只有一个 turn 且有两条用户消息。

调用关系:覆盖 handle_turn_started 设置 opened_explicitly 后 handle_user_message 不拆轮的逻辑。

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

tests::reconstructs_tool_items_from_persisted_completion_events2348–2459 ↗
fn reconstructs_tool_items_from_persisted_completion_events()

作用:测试搜索、命令、MCP 工具这些完成事件能直接恢复成历史条目。

数据流:构造 WebSearchEnd、ExecCommandEnd、McpToolCallEnd → 重放 → 检查三个工具条目的字段。

调用关系:覆盖 handle_web_search_end、handle_exec_command_end、handle_mcp_tool_call_end。

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

tests::reconstructs_mcp_tool_result_meta_from_persisted_completion_events2462–2525 ↗
fn reconstructs_mcp_tool_result_meta_from_persisted_completion_events()

作用:测试 MCP 工具结果里的 meta 和结构化内容不会丢。

数据流:输入成功的 McpToolCallEnd,带 content、structured_content、meta → 重建 → 检查结果完整。

调用关系:验证 handle_mcp_tool_call_end 对成功结果的保存。

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

tests::reconstructs_dynamic_tool_items_from_request_and_response_events2528–2593 ↗
fn reconstructs_dynamic_tool_items_from_request_and_response_events()

作用:测试动态工具的请求和响应会合并成一个完成条目。

数据流:输入 DynamicToolCallRequest 再输入 Response → 重建 → 检查同一 id 的条目状态为 Completed 且带内容。

调用关系:覆盖 handle_dynamic_tool_call_request、handle_dynamic_tool_call_response 和 upsert_turn_item。

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

tests::reconstructs_declined_exec_and_patch_items2596–2685 ↗
fn reconstructs_declined_exec_and_patch_items()

作用:测试被拒绝的命令和补丁会在历史里显示为 Declined。

数据流:构造 declined 命令结束和 declined patch 结束 → 重放 → 检查命令和文件变更状态。

调用关系:验证命令和文件修改构建函数与对应 handle_*_end 的集成。

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

tests::reconstructs_declined_guardian_command_item2688–2771 ↗
fn reconstructs_declined_guardian_command_item()

作用:测试安全审查拒绝命令时,会显示成被拒绝的命令执行条目。

数据流:输入 GuardianAssessment 进行中和 Denied → 重放 → 检查 CommandExecution 状态为 Declined。

调用关系:覆盖 handle_guardian_assessment 对审查状态的映射。

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

tests::reconstructs_in_progress_guardian_execve_item2774–2837 ↗
fn reconstructs_in_progress_guardian_execve_item()

作用:测试安全审查中的 execve 操作能显示为进行中的命令。

数据流:输入 InProgress 的 GuardianAssessment execve → 重建 → 检查命令字符串和 InProgress 状态。

调用关系:验证 build_item_from_guardian_event 与 handle_guardian_assessment 的配合。

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

tests::assigns_late_exec_completion_to_original_turn2840–2935 ↗
fn assigns_late_exec_completion_to_original_turn()

作用:测试迟到的命令完成事件会回到原本的回合。

数据流:构造 turn-a 完成、turn-b 开始后才收到 turn-a 的 ExecCommandEnd → 重放 → 检查命令在 turn-a。

调用关系:验证 handle_exec_command_end 使用事件 turn_id,而不是当前回合。

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

tests::drops_late_turn_scoped_item_for_unknown_turn_id2938–3027 ↗
fn drops_late_turn_scoped_item_for_unknown_turn_id()

作用:测试带未知 turn_id 的迟到条目会被丢弃,不会污染当前回合。

数据流:构造两个回合,再输入 turn-missing 的命令结束 → finish → 检查两个回合都没有这条命令。

调用关系:覆盖 upsert_item_in_turn_id 找不到回合时 warn 并丢弃的行为。

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

tests::patch_apply_begin_updates_active_turn_snapshot_with_file_change3030–3095 ↗
fn patch_apply_begin_updates_active_turn_snapshot_with_file_change()

作用:测试补丁开始事件会立刻体现在当前回合快照里。

数据流:构造 TurnStarted、UserMessage、PatchApplyBegin → 查询 active_turn_snapshot → 检查 FileChange 为 InProgress。

调用关系:验证 handle_patch_apply_begin、upsert_item_in_turn_id 和 active_turn_snapshot。

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

tests::apply_patch_approval_request_updates_active_turn_snapshot_with_file_change3098–3165 ↗
fn apply_patch_approval_request_updates_active_turn_snapshot_with_file_change()

作用:测试补丁审批请求也会更新当前回合快照。

数据流:输入审批请求事件 → 查询活跃回合快照 → 检查文件变更条目已经出现。

调用关系:覆盖 handle_apply_patch_approval_request 的实时展示路径。

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

tests::late_turn_complete_does_not_close_active_turn3168–3237 ↗
fn late_turn_complete_does_not_close_active_turn()

作用:测试旧回合迟到的完成事件不会关闭正在进行的新回合。

数据流:turn-a 完成,turn-b 开始后又收到 turn-a complete → 继续加消息 → 重建后检查 turn-b 内容完整。

调用关系:验证 handle_turn_complete 优先精确匹配已完成回合,不误关 current_turn。

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

tests::late_turn_aborted_does_not_interrupt_active_turn3240–3302 ↗
fn late_turn_aborted_does_not_interrupt_active_turn()

作用:测试旧回合迟到的中断事件不会把新回合标成中断。

数据流:turn-a 完成,turn-b 开始后收到 turn-a aborted → 再加消息 → 检查 turn-b 仍是 InProgress。

调用关系:覆盖 handle_turn_aborted 的按 id 精确处理逻辑。

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

tests::preserves_compaction_only_turn3305–3342 ↗
fn preserves_compaction_only_turn()

作用:测试只有压缩标记、没有可展示条目的明确回合也会被保留。

数据流:输入 TurnStarted、Compacted、TurnComplete → 重建 → 检查得到一个空 items 的完成回合。

调用关系:验证 handle_compacted 和 finish_current_turn 的 saw_compaction 保留逻辑。

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

tests::reconstructs_collab_resume_end_item3345–3397 ↗
fn reconstructs_collab_resume_end_item()

作用:测试协作代理恢复结束事件能恢复成历史条目。

数据流:输入用户消息和 CollabResumeEnd → 重建 → 检查 ResumeAgent 条目的发送方、接收方和状态。

调用关系:覆盖 handle_collab_resume_end。

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

tests::reconstructs_collab_spawn_end_item_with_model_metadata3400–3457 ↗
fn reconstructs_collab_spawn_end_item_with_model_metadata()

作用:测试创建协作代理结束时,模型和推理力度等元数据会保存。

数据流:输入 CollabAgentSpawnEnd,带新线程、模型、prompt、reasoning_effort → 重建 → 检查 SpawnAgent 条目完整。

调用关系:覆盖 handle_collab_agent_spawn_end 的成功路径。

调用图:调用 2 个内部函数(build_turns_from_rollout_items, try_from);外部调用 2 个(assert_eq!, vec!)。

tests::reconstructs_interrupted_send_input_as_completed_collab_call3460–3529 ↗
fn reconstructs_interrupted_send_input_as_completed_collab_call()

作用:测试给子代理重定向输入时,子代理状态可为 Interrupted,但工具调用本身仍算完成。

数据流:输入 interaction begin 和 status Interrupted 的 end → 重建 → 检查工具状态 Completed,代理状态 Interrupted。

调用关系:验证 handle_collab_agent_interaction_end 对 AgentStatus::Interrupted 的特殊语义。

调用图:调用 2 个内部函数(build_turns_from_rollout_items, try_from);外部调用 2 个(assert_eq!, vec!)。

tests::rollback_failed_error_does_not_mark_turn_failed3532–3561 ↗
fn rollback_failed_error_does_not_mark_turn_failed()

作用:测试“回滚失败”这种错误不会把正常对话轮标成失败。

数据流:输入正常用户和模型消息,再输入 ThreadRollbackFailed 错误 → 重建 → 检查 turn 仍 Completed 且无 error。

调用关系:覆盖 handle_error 里 affects_turn_status 的过滤效果。

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

tests::out_of_turn_error_does_not_create_or_fail_a_turn3564–3620 ↗
fn out_of_turn_error_does_not_create_or_fail_a_turn()

作用:测试回合之外的请求级错误不会新建或修改回合。

数据流:先完成一个 turn,再输入 BadRequest 错误 → 重放 → 检查原 turn 不受影响。

调用关系:验证 handle_error 只有当前回合存在且错误影响回合时才改状态。

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

tests::error_then_turn_complete_preserves_failed_status3623–3675 ↗
fn error_then_turn_complete_preserves_failed_status()

作用:测试先失败再收到完成事件时,失败状态不会被覆盖成完成。

数据流:输入 TurnStarted、UserMessage、Error、TurnComplete → 重建 → 检查状态仍 Failed 且错误信息保留。

调用关系:覆盖 handle_error 与 handle_turn_complete 的状态保护。

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

tests::rebuilds_hook_prompt_items_from_rollout_response_items3678–3730 ↗
fn rebuilds_hook_prompt_items_from_rollout_response_items()

作用:测试 rollout 里的特殊 hook prompt 响应项能恢复成 HookPrompt 条目。

数据流:构造 hook prompt ResponseItem → 重放 → 检查回合里有 HookPrompt 和两个 fragment。

调用关系:覆盖 handle_response_item 与 parse_hook_prompt_message 的集成。

调用图:调用 2 个内部函数(build_turns_from_rollout_items, build_hook_prompt_message);外部调用 3 个(from_single_hook, assert_eq!, vec!)。

tests::ignores_plain_user_response_items_in_rollout_replay3733–3763 ↗
fn ignores_plain_user_response_items_in_rollout_replay()

作用:测试普通用户 ResponseItem 不会被误当成 hook prompt 放进历史。

数据流:输入普通 user ResponseItem → 重建 → 检查回合 items 为空。

调用关系:验证 handle_response_item 对非 hook prompt 的忽略逻辑。

调用图:调用 1 个内部函数(build_turns_from_rollout_items);外部调用 3 个(assert!, assert_eq!, vec!)。

tests::changed_rollout_item_reports_new_item_snapshot3766–3805 ↗
fn changed_rollout_item_reports_new_item_snapshot()

作用:测试单条 rollout 处理能报告新建条目的快照。

数据流:新 builder 处理一条 UserMessage with changes → 得到变更集 → 检查包含新 UserMessage 和新回合状态。

调用关系:覆盖 handle_rollout_item_with_changes、collect_changes、record_changed_item 和 record_changed_pending_turn。

调用图:调用 1 个内部函数(new);外部调用 5 个(default, new, assert_eq!, UserMessage, EventMsg)。

tests::changed_rollout_item_reports_updated_existing_item_snapshot3808–3845 ↗
fn changed_rollout_item_reports_updated_existing_item_snapshot()

作用:测试同一条目被更新时,增量结果会报告更新后的快照。

数据流:先处理 WebSearchBegin,再处理 WebSearchEnd with changes → 检查变更集里是完整搜索结果。

调用关系:验证 upsert_item_in_current_turn 和 record_changed_item。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, WebSearchBegin, WebSearchEnd, EventMsg)。

tests::changed_rollout_item_reports_streaming_item_mutation3848–3877 ↗
fn changed_rollout_item_reports_streaming_item_mutation()

作用:测试流式追加思考内容时,会报告被修改后的条目。

数据流:先处理 reasoning summary,再处理 raw content with changes → 检查变更集里 Reasoning 同时有 summary 和 content。

调用关系:覆盖 handle_agent_reasoning_raw_content 对已有条目的 mutation 记录。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, AgentReasoning, AgentReasoningRawContent, EventMsg)。

tests::changed_rollout_item_reports_turn_completion_metadata3880–3943 ↗
fn changed_rollout_item_reports_turn_completion_metadata()

作用:测试回合开始和完成的时间状态变化会出现在增量变更里。

数据流:处理 TurnStarted with changes,再处理用户消息和 TurnComplete with changes → 检查 started_at、completed_at、duration_ms。

调用关系:覆盖 record_changed_pending_turn 和 handle_turn_complete 的变更记录。

调用图:调用 1 个内部函数(new);外部调用 7 个(default, new, assert_eq!, TurnComplete, TurnStarted, UserMessage, EventMsg)。

tests::changed_rollout_items_dedupe_updated_item_snapshots3946–3987 ↗
fn changed_rollout_items_dedupe_updated_item_snapshots()

作用:测试批量变更会把同一条目的 begin/end 压缩成最终快照。

数据流:批量处理 WebSearchBegin 和 WebSearchEnd → 得到一个变更集 → 检查只报告完整搜索条目一次。

调用关系:验证 ThreadHistoryChangeAccumulator::push_item_change 的去重。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, WebSearchBegin, WebSearchEnd, EventMsg)。

tests::changed_rollout_items_dedupe_turn_metadata_snapshots3990–4024 ↗
fn changed_rollout_items_dedupe_turn_metadata_snapshots()

作用:测试批量变更会把同一回合的开始和完成压缩成最终回合状态。

数据流:批量处理 TurnStarted 和 TurnComplete → 检查只输出 Completed 的最终回合快照。

调用关系:验证 ThreadHistoryChangeAccumulator::push_turn_change。

调用图:调用 1 个内部函数(new);外部调用 5 个(default, assert_eq!, TurnComplete, TurnStarted, EventMsg)。

tests::changed_rollout_items_drop_prior_changes_for_removed_turns4027–4058 ↗
fn changed_rollout_items_drop_prior_changes_for_removed_turns()

作用:测试批量处理中如果回合后来被回滚,之前为它累计的新增变化会被删除。

数据流:批量处理 TurnStarted、UserMessage、ThreadRolledBack → 检查没有 changed_items 和 changed_turns,只剩 removed_turn_ids。

调用关系:覆盖 ThreadHistoryChangeAccumulator::push_removed_turn_id 对已删除回合的清理。

调用图:调用 1 个内部函数(new);外部调用 7 个(default, new, assert_eq!, ThreadRolledBack, TurnStarted, UserMessage, EventMsg)。

Rollout 与元数据持久化

这些文件定义要记录的内容,重建持久化会话,派生标准化元数据,并在 rollout 文件和数据库之间同步线程存储状态。

app-server/src/request_processors/external_agent_session_import.rs源码 ↗
orchestrationconfig import / background session import

可以把它理解成一个“旧聊天记录搬家工”。外部智能体的会话文件不能直接塞进系统,因为里面可能格式不对、已经导入过,或者缺少本系统需要的线程信息。这个文件先拿到导入许可,防止多个导入任务同时挤在一起;然后最多同时处理 5 个会话。每个会话会先做准备和校验,这一步放到后台阻塞任务里跑,避免卡住异步运行环境。准备好后,它会按当前配置创建一个新线程,补上模型、工作目录、标题、首条用户消息、时间、记忆开关等元数据,再把真正需要持久保存的对话条目写进去。全部完成后,它会记录“哪些源文件已经成功导入”的账本;失败时也会把失败阶段和原因写回导入结果,方便用户知道是哪一步出了问题。

函数细节5
ExternalAgentSessionImporter::new46–61 ↗
fn new(
        codex_home: PathBuf,
        thread_manager: Arc<ThreadManager>,
        thread_store: Arc<dyn ThreadStore>,
        config_manager: ConfigManager,
        arg0_paths: Arg0DispatchPath

作用:创建一个会话导入器,把之后导入所需的路径、线程服务、配置服务和可执行文件路径都装进去。别人要开始导入外部会话前,先用它组装出这个“搬家工”。

数据流:输入是 Codex 主目录、线程管理器、线程存储、配置管理器和启动相关路径 → 它把这些保存到结构体里,并新建一个信号量(一种排队锁,用来限制同一时间能有多少导入批次)→ 输出一个可以复用、可以克隆的 ExternalAgentSessionImporter。

调用关系:它通常在上层初始化导入流程时被调用。后面的 import_sessions、prepare_session_import、persist_session 都依赖这里保存好的服务和路径来工作。

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

ExternalAgentSessionImporter::import_sessions63–118 ↗
async fn import_sessions(
        &self,
        sessions: Vec<ExternalAgentSessionMigration>,
        mut item_result: ExternalAgentConfigImportItemResult,
    ) -> ExternalAgentConfigImportItemResul

作用:导入一批外部会话,并把每个会话成功或失败的结果写进返回对象里。它是这个文件里对外最重要的批量入口。

数据流:输入是一组待迁移会话和一个用来记录结果的 item_result → 如果列表为空就原样返回;否则先申请导入许可,再把会话变成异步任务,最多同时跑 5 个;每个任务成功就记录源文件和新线程 ID,失败就记录失败阶段和错误信息;最后把所有成功导入写到账本 → 输出更新后的 item_result,同时可能更新导入账本。

调用关系:它负责把一批导入任务分发给 import_requested_session。单个会话完成后,它调用结果记录方法保存成功信息,遇到错误时交给 record_import_error 记录;整批结束后交给 record_completed_session_imports 保存“已导入”的历史记录。

调用图:调用 2 个内部函数(record_success, record_import_error);外部调用 4 个(new, record_completed_session_imports, pin_mut!, iter)。

ExternalAgentSessionImporter::import_requested_session120–149 ↗
async fn import_requested_session(
        &self,
        session: ExternalAgentSessionMigration,
    ) -> Result<Option<CompletedExternalAgentSessionImport>, SessionImportFailure>

作用:处理单个外部会话:先检查和整理,再真正写入系统线程库。它把“一个文件能不能导入、导入后变成哪个线程”这件事串起来。

数据流:输入是一个外部会话迁移请求 → 它先记住源文件路径,然后调用 prepare_session_import 做校验和整理;如果发现不需要导入,就返回空;如果准备成功,就调用 persist_session 创建新线程并写入内容 → 输出成功导入记录,里面包含源文件、源内容校验值和新线程 ID;如果失败,则带上出错阶段返回错误。

调用关系:它由 import_sessions 为每个会话调用。它自己不做复杂细节,而是把前半段交给 prepare_session_import,把后半段交给 persist_session;这样批量流程能清楚地区分是“准备失败”还是“保存失败”。

调用图:调用 2 个内部函数(persist_session, prepare_session_import)。

ExternalAgentSessionImporter::prepare_session_import151–160 ↗
async fn prepare_session_import(
        &self,
        session: ExternalAgentSessionMigration,
    ) -> Result<Option<PendingSessionImport>, String>

作用:把外部会话文件变成系统可以导入的临时对象,并顺便做格式和重复导入等检查。它存在的原因是:导入前必须先确认这份材料干净、有效。

数据流:输入是一个外部会话迁移请求,以及导入器里保存的 Codex 主目录 → 它把真正的准备工作放进 spawn_blocking 后台阻塞任务中执行,调用 prepare_validated_session_import 读取、校验并整理会话 → 输出一个 PendingSessionImport;如果文件不需要导入则输出空;如果任务崩了或校验失败,则输出可读的错误文字。

调用关系:它只被 import_requested_session 调用,负责导入链路的第一关。因为准备过程可能读文件、算校验值或做较重检查,所以它用 spawn_blocking 避免堵住主异步任务。

调用图:被 1 处调用(import_requested_session);外部调用 2 个(clone, spawn_blocking)。

ExternalAgentSessionImporter::persist_session162–279 ↗
async fn persist_session(
        &self,
        session: ImportedExternalAgentSession,
    ) -> Result<ThreadId, String>

作用:把已经整理好的外部会话真正保存成本系统的一条新对话线程。它负责补齐系统运行需要的配置、模型信息和线程元数据。

数据流:输入是 ImportedExternalAgentSession,里面有工作目录、标题、第一条用户消息和对话条目 → 它按该工作目录加载配置,查默认模型和模型说明,新建线程 ID,设置来源、记忆模式、创建时间、标题、预览、版本号等信息;然后创建线程、追加可持久保存的对话条目、更新元数据、落盘保存并关闭线程 → 输出新线程 ID;如果追加条目失败,会丢弃刚创建的线程,避免留下半截坏数据。

调用关系:它由 import_requested_session 在准备成功后调用,是导入链路的落库阶段。它会使用配置管理器加载配置,使用线程管理器取得模型信息,再把创建、追加、更新、持久化和关闭这些动作交给 thread_store 完成。

调用图:调用 2 个内部函数(load_with_overrides, new);被 1 处调用(import_requested_session);外部调用 5 个(default, now, new, env!, format!)。

app-server/src/request_processors/thread_resume_redaction.rs源码 ↗
domain_logicrequest handling,主要在 thread/resume 响应发给特定远程客户端前生效

有些客户端在调用 thread/resume,也就是“把一段旧对话重新取回来继续用”时,可能会收到很大的 MCP 工具调用内容或图片生成结果。MCP 可以理解成模型调用外部工具的通道;这些工具参数、结果里可能又大又敏感。这个文件做的是一个临时补丁:只在发给特定远程客户端的响应里动手脚,不改数据库里的历史,也不影响模型自己恢复上下文。它会判断客户端名字是不是 ChatGPT 的 Android 或 iOS 远程客户端;如果是,就遍历每个对话回合里的项目。普通消息保留,MCP 工具调用保留外壳但把参数、结果、错误信息替换成“[redacted]”,图片生成项目则直接删掉。可以把它想成寄包裹前临时把危险品拿掉、把隐私文件用黑笔涂掉,但仓库里的原件还在。

函数细节6
should_redact_thread_resume_payloads13–15 ↗
fn should_redact_thread_resume_payloads(client_name: Option<&str>) -> bool

作用:判断这次 thread/resume 响应要不要做“打码”。它只认几个特定的 ChatGPT 远程客户端名字,避免影响其他客户端。

数据流:输入是一个可能存在、也可能为空的客户端名字 → 它检查这个名字是不是在预先写好的远程客户端名单里 → 输出 true 或 false,表示后面要不要遮掉大 payload。它不修改任何数据。

调用关系:这个函数像门口保安,先决定某个客户端是否需要特殊处理。真正动手遮内容的是 redact_thread_resume_payloads;这个判断通常会在调用它之前使用。

redact_thread_resume_payloads17–39 ↗
fn redact_thread_resume_payloads(turns: &mut [Turn])

作用:把一批对话回合里不适合直接发给远程客户端的大内容或敏感内容处理掉。它会保留普通文字消息,遮掉 MCP 工具调用的细节,并移除图片生成结果。

数据流:输入是一组可修改的 Turn,也就是对话里的多个回合 → 它逐个查看每个回合里的 ThreadItem:遇到 MCP 工具调用,就把 arguments 改成字符串“[redacted]”,如果有 result 就换成一个只含“[redacted]”的简化结果,如果有 error 就把错误消息也改成“[redacted]”;遇到 ImageGeneration 就从列表里删掉;其他项目保持原样 → 输出不是新对象,而是直接把传入的对话回合改好。

调用关系:这是这个文件的核心清理动作。测试函数 tests::redacts_mcp_success_result_and_removes_image_generation 和 tests::redacts_mcp_error_message 都会调用它,分别确认成功结果、图片生成、错误消息这些情况都被按预期处理。处理 MCP 结果时,它会用 redacted_mcp_tool_call_result 造一个安全的替代结果。

调用图:被 2 处调用(redacts_mcp_error_message, redacts_mcp_success_result_and_removes_image_generation)。

redacted_mcp_tool_call_result41–50 ↗
fn redacted_mcp_tool_call_result() -> McpToolCallResult

作用:生成一个“安全版”的 MCP 工具调用结果。这个结果只告诉客户端内容被遮掉了,不再携带真实工具返回的数据。

数据流:它不需要外部输入 → 它创建一个 McpToolCallResult,里面只有一段 JSON 文本内容,文本是“[redacted]”,结构化内容和额外元数据都设为空 → 返回这个干净的替代结果。

调用关系:它是 redact_thread_resume_payloads 的小帮手。主函数发现某个 MCP 工具调用原本有 result 时,就用它做一个替身,避免把原始结果发出去。内部会构造一个列表来放这段替代文本。

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

tests::redacts_mcp_success_result_and_removes_image_generation67–130 ↗
fn redacts_mcp_success_result_and_removes_image_generation()

作用:测试“成功的 MCP 工具调用”和“图片生成结果”会不会被正确处理。它保证普通消息还在,MCP 内容被打码,图片生成项目被删掉。

数据流:它先造一个假的线程,里面有普通助手消息、一个成功的 MCP 工具调用、一个图片生成项目 → 调用 redact_thread_resume_payloads 修改这个线程 → 再用断言检查:列表只剩两个项目,普通消息没变,MCP 的参数和结果变成“[redacted]”,图片生成项目不见了。

调用关系:这是给核心函数 redact_thread_resume_payloads 上保险的测试。它通过 tests::test_thread 造测试用线程,再调用被测函数,最后用 assert_eq! 对比处理后的数据是不是完全符合预期。

调用图:调用 1 个内部函数(redact_thread_resume_payloads);外部调用 3 个(test_thread, assert_eq!, vec!)。

tests::redacts_mcp_error_message133–168 ↗
fn redacts_mcp_error_message()

作用:测试 MCP 工具调用失败时,错误消息也会被打码。这样即使工具报错里带了敏感信息,也不会直接发给目标客户端。

数据流:它先造一个只包含失败 MCP 工具调用的假线程,参数和错误消息里都有“secret”内容 → 调用 redact_thread_resume_payloads → 检查这个项目仍然存在,但参数和错误 message 都被替换成“[redacted]”,其他像工具名、状态、耗时等外壳信息保持不变。

调用关系:这个测试补上了失败场景,和 tests::redacts_mcp_success_result_and_removes_image_generation 一起覆盖主要分支。它同样依赖 tests::test_thread 创建样本数据,并直接验证 redact_thread_resume_payloads 的效果。

调用图:调用 1 个内部函数(redact_thread_resume_payloads);外部调用 3 个(test_thread, assert_eq!, vec!)。

tests::test_thread170–202 ↗
fn test_thread(items: Vec<ThreadItem>) -> Thread

作用:给测试快速造一个假的 Thread,也就是一整条对话线程。调用者只需要传入想测试的项目列表,不用每次手写一大堆固定字段。

数据流:输入是一组 ThreadItem,也就是要放进测试对话回合里的项目 → 它填好线程 id、会话 id、状态、路径、版本、来源等测试用默认值,并把这些项目放进一个已完成的 Turn 里 → 返回一个完整的 Thread,供测试函数拿去修改和检查。

调用关系:它是测试里的搭积木工具,不参与线上业务。两个测试函数都会用它准备数据;它内部会用 test_path_buf 生成测试路径,并用 vec! 组装回合列表。

调用图:外部调用 2 个(test_path_buf, vec!)。

app-server/src/request_processors/thread_summary.rs源码 ↗
domain_logicrequest handling / cross-cutting;部分读取 rollout 的函数只在测试时使用

这个文件像一个“翻译员”。系统内部保存的是 rollout 文件、核心协议里的会话来源、权限配置、沙箱策略等比较底层的信息;外部接口需要的却是线程列表、线程启动通知、线程设置这些好理解的数据。它会从会话记录里提取第一条用户消息当预览,补上创建时间、更新时间、模型提供方、Git 信息和工作目录;也会把内部权限配置转成接口能返回的格式。这里还特别处理了子代理线程的信息:如果一个线程是由另一个线程派生出来的,它会把代理昵称和角色补进来源里。部分函数只在测试时编译,用来从本地记录文件还原摘要,方便验证线程列表的行为是否正确。

函数细节12
read_summary_from_rollout9–81 ↗
async fn read_summary_from_rollout(
    path: &Path,
    fallback_provider: &str,
) -> std::io::Result<ConversationSummary>

作用:从一个 rollout 会话记录文件里读出线程摘要。它主要用于测试,模拟系统从磁盘上的历史记录恢复出线程列表时会看到什么。

数据流:输入是记录文件路径和一个备用的模型提供方名字。它先读取文件开头的几行,确认第一行是会话元数据;再补齐子代理的昵称和角色,读取文件修改时间作为更新时间;如果能从前几行找到用户消息,就交给 extract_conversation_summary 生成带预览的摘要。找不到预览时,它仍然会用元数据拼出一个摘要,只是 preview 为空;如果文件为空或开头不是元数据,就返回错误。

调用关系:它是测试里读取历史会话摘要的入口。流程中会调用 with_thread_spawn_agent_metadata 修正来源信息,调用 read_updated_at 获取更新时间,然后优先调用 extract_conversation_summary 抽取更完整的摘要。

调用图:调用 3 个内部函数(extract_conversation_summary, read_updated_at, with_thread_spawn_agent_metadata);外部调用 4 个(to_path_buf, new, other, format!)。

extract_conversation_summary84–130 ↗
fn extract_conversation_summary(
    path: PathBuf,
    head: &[serde_json::Value],
    session_meta: &SessionMeta,
    git: Option<&CoreGitInfo>,
    fallback_provider: &str,
    updated_at: Option<S

作用:从已经读到的 rollout 文件开头内容里,提取一份真正能展示在线程列表里的摘要。它最关键的工作是找到第一条用户消息,拿来当预览文字。

数据流:输入是文件路径、文件开头的 JSON 行、会话元数据、Git 信息、备用模型提供方和更新时间。它逐条尝试把 JSON 转成响应项,再找出第一条用户消息;如果消息里带有内部标记,就把标记前面的部分去掉,只留下用户真正输入的内容。最后把会话编号、时间、路径、预览、模型提供方、工作目录、版本、来源和 Git 信息打包成 ConversationSummary;如果没找到用户消息,就返回 None。

调用关系:它被 read_summary_from_rollout 调用,是“能不能做出带预览摘要”的判断点。它自己不读文件,只处理 read_summary_from_rollout 已经拿到的数据。

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

map_git_info133–139 ↗
fn map_git_info(git_info: &CoreGitInfo) -> ConversationGitInfo

作用:把核心层的 Git 信息换成线程摘要里使用的 Git 信息格式。Git 是版本管理工具,这里只保留提交号、分支名和远程仓库地址这些展示时常用的内容。

数据流:输入是一份 CoreGitInfo。它读取提交哈希、分支和仓库地址,把字段名和外部接口需要的结构对齐,输出 ConversationGitInfo;原数据不会被修改。

调用关系:它是一个小转换器,供摘要生成流程使用。read_summary_from_rollout 和 extract_conversation_summary 在整理摘要时会间接依赖它把 Git 数据变成接口格式。

with_thread_spawn_agent_metadata141–170 ↗
fn with_thread_spawn_agent_metadata(
    source: codex_protocol::protocol::SessionSource,
    agent_nickname: Option<String>,
    agent_role: Option<String>,
) -> codex_protocol::protocol::SessionSour

作用:给“由线程派生出来的子代理线程”补上代理昵称和代理角色。简单说,就是如果一个子任务有自己的名字和职责,这里确保这些信息不会丢。

数据流:输入是原来的会话来源,以及可选的 agent_nickname 和 agent_role。若两者都没有,就原样返回来源。若来源确实是 ThreadSpawn 类型的子代理来源,它会优先使用新传入的昵称和角色;没有新值时保留旧值。若来源不是这种子代理线程,也原样返回。

调用关系:它被 read_summary_from_rollout 调用,用在从旧记录恢复摘要之前。这样后续生成的线程摘要、线程对象就能正确显示代理昵称和角色。

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

thread_response_active_permission_profile172–176 ↗
fn thread_response_active_permission_profile(
    active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
) -> Option<codex_app_server_protocol::ActivePermissionProfile>

作用:把内部的“当前生效权限配置”转换成接口返回用的格式。权限配置可以理解为系统允许这条线程做哪些事的规则。

数据流:输入是一个可能存在的 ActivePermissionProfile。它如果有值,就用通用转换规则转成 app-server 协议里的 ActivePermissionProfile;如果没有值,就保持 None。输出也是可选值,不修改原数据。

调用关系:它被 thread_settings_from_config_snapshot 和 thread_settings_from_core_snapshot 调用。两个设置转换流程都会用它来保持权限字段的外部格式一致。

调用图:被 2 处调用(thread_settings_from_config_snapshot, thread_settings_from_core_snapshot)。

thread_response_sandbox_policy178–187 ↗
fn thread_response_sandbox_policy(
    permission_profile: &codex_protocol::models::PermissionProfile,
    cwd: &Path,
) -> codex_app_server_protocol::SandboxPolicy

作用:根据权限配置和当前工作目录,算出接口要返回的沙箱策略。沙箱可以理解为“安全围栏”,限制程序能访问哪些文件、能做哪些危险操作。

数据流:输入是权限配置和当前工作目录 cwd。它先调用沙箱兼容层,根据权限规则和目录算出实际沙箱策略;再把这个内部策略转换成 app-server 协议里的 SandboxPolicy。输出是可直接返回给客户端的沙箱策略。

调用关系:它被 thread_settings_from_config_snapshot 和 thread_settings_from_core_snapshot 调用。凡是要把线程设置返回给外部时,都需要它把底层权限规则变成清楚的沙箱说明。

调用图:被 2 处调用(thread_settings_from_config_snapshot, thread_settings_from_core_snapshot);外部调用 1 个(compatibility_sandbox_policy_for_permission_profile)。

thread_settings_from_config_snapshot189–211 ↗
fn thread_settings_from_config_snapshot(
    config_snapshot: &ThreadConfigSnapshot,
) -> ThreadSettings

作用:把配置快照转换成线程设置。配置快照就是某一刻线程使用的模型、权限、工作目录、协作模式等设置的“照片”。

数据流:输入是 ThreadConfigSnapshot。它读取工作目录、审批策略、审批人、权限配置、模型、模型提供方、服务档位、推理强度、摘要模式、协作模式和个性设定;其中权限相关字段会分别交给 thread_response_sandbox_policy 和 thread_response_active_permission_profile 转成接口格式。最后输出一个 ThreadSettings,供外部接口展示或使用。

调用关系:它是从配置系统到线程接口的桥。需要根据当前配置生成线程设置时会用它,它内部把权限和沙箱这两块转换工作交给专门的小函数。

调用图:调用 3 个内部函数(thread_response_active_permission_profile, thread_response_sandbox_policy, cwd)。

thread_settings_from_core_snapshot213–247 ↗
fn thread_settings_from_core_snapshot(
    snapshot: codex_protocol::protocol::ThreadSettingsSnapshot,
) -> ThreadSettings

作用:把核心协议里的线程设置快照转换成 app-server 返回的线程设置。它解决的是“核心层说法”和“接口层说法”不完全一样的问题。

数据流:输入是一份 ThreadSettingsSnapshot。它先把快照拆开,拿到模型、模型提供方、服务档位、审批策略、权限配置、当前权限、工作目录、推理设置、个性和协作模式等字段;再根据权限配置和工作目录算沙箱策略,并转换当前权限格式。最后输出 ThreadSettings。

调用关系:它常用于核心层已经给出完整线程设置时。和 thread_settings_from_config_snapshot 类似,它也调用 thread_response_sandbox_policy 和 thread_response_active_permission_profile 来统一权限相关输出。

调用图:调用 2 个内部函数(thread_response_active_permission_profile, thread_response_sandbox_policy)。

parse_datetime250–256 ↗
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>>

作用:把字符串形式的时间转成程序能计算的 UTC 时间。UTC 是统一标准时间,方便不同地区的时间比较。

数据流:输入是一个可选的时间字符串。它如果存在,就尝试按 RFC3339 格式解析;RFC3339 是常见的类似“2024-01-01T12:00:00Z”的时间写法。解析成功后转成 UTC 时间,失败或没有输入就返回 None。

调用关系:它被 summary_to_thread 调用,用来把摘要里的创建时间和更新时间变成线程对象里的数字时间戳。

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

read_updated_at259–269 ↗
async fn read_updated_at(path: &Path, created_at: Option<&str>) -> Option<String>

作用:读取 rollout 文件的最后修改时间,把它当作线程的更新时间。这样历史线程列表可以按最近变化来显示,而不只看创建时间。

数据流:输入是文件路径和一个可选的创建时间。它尝试读取文件元数据,再取文件系统记录的修改时间,并格式化成带毫秒的标准时间字符串。如果文件元数据读不到,或者修改时间取不到,就退回使用传入的创建时间;如果创建时间也没有,就返回 None。

调用关系:它被 read_summary_from_rollout 调用,属于从磁盘记录恢复摘要时的辅助步骤。它只负责时间,不负责解析会话内容。

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

thread_started_notification271–274 ↗
fn thread_started_notification(mut thread: Thread) -> ThreadStartedNotification

作用:生成“线程已启动”的通知,并且在通知里去掉具体对话轮次。这样启动通知只告诉客户端线程本身的信息,不顺带塞进完整聊天记录。

数据流:输入是一个 Thread。它先清空 thread.turns,也就是清掉线程里的对话轮次列表;然后把这个精简后的线程放进 ThreadStartedNotification 里输出。传入的线程会被消费并变成通知内容。

调用关系:它用于线程启动事件的发送阶段。上游准备好 Thread 后调用它,下游拿到的是一个适合广播给客户端的启动通知。

summary_to_thread277–335 ↗
fn summary_to_thread(
    summary: ConversationSummary,
    fallback_cwd: &AbsolutePathBuf,
) -> Thread

作用:把 ConversationSummary 转成完整的 Thread 对象。它主要用于测试或恢复场景:先有一份轻量摘要,再把它包装成线程列表里统一使用的线程结构。

数据流:输入是一份会话摘要和一个备用工作目录。它把摘要拆开,解析创建时间和更新时间,转换 Git 信息,规范化工作目录;如果工作目录无法规范化,就记录警告并使用备用目录。然后把会话编号转成线程 id,填入预览、模型提供方、时间戳、状态、路径、CLI 版本、代理昵称、代理角色、来源和 Git 信息,最后输出一个 Thread。输出的线程状态是 NotLoaded,表示只知道概要,还没有载入完整对话内容。

调用关系:它在摘要和线程对象之间搭桥。它会调用 parse_datetime 处理时间,也会调用路径规范化工具处理 cwd;生成的 Thread 可以继续被线程列表、通知或测试断言使用。

调用图:调用 2 个内部函数(parse_datetime, relative_to_current_dir);外部调用 2 个(new, normalize_for_native_workdir)。

core/src/session/rollout_reconstruction.rs源码 ↗
domain_logicsession restore / resume / fork

可以把 rollout 理解成一本流水账:用户说了什么、模型回了什么、某一轮用了什么设置、历史有没有被压缩、有没有撤回几轮。这个文件的工作,就是从这本流水账里还原出一份干净的当前聊天历史。它先倒着看记录,因为最新的记录最有用:比如最近一次历史压缩留下的 replacement_history 就像一个存档点,找到了它,前面更老的记录通常就不用再逐条重放。倒着看时,它把同一轮对话临时装进 ActiveReplaySegment,判断这轮是不是真正的用户轮次,并收集上一轮设置、上下文基准和窗口编号。遇到回滚时,它会跳过最新的若干个用户轮次。之后它再正着重放剩下的尾巴,把消息重新塞进 ContextManager(一种维护上下文列表的工具),遇到压缩、跨代理通信、回滚也按原来的含义处理。最后产出 RolloutReconstruction:包含重建后的历史、恢复会话需要的上一轮设置、参考上下文,以及窗口编号。

函数细节3
turn_ids_are_compatible40–43 ↗
fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&str>) -> bool

作用:这个小函数用来判断两个“轮次编号”能不能算是同一轮。它允许其中一边没有编号,因为旧记录或某些事件可能缺少这个信息。

数据流:输入是当前正在整理的轮次编号和某条记录自带的轮次编号,二者都可能为空。它检查:如果已有编号,就要求另一边为空或完全相同;如果没有已有编号,就认为不冲突。输出是一个真假值,告诉上层这条记录能不能合到当前这段回放里。

调用关系:Session::reconstruct_history_from_rollout 在倒着扫描 rollout 时会反复用它,尤其是把 TurnContext 或 TurnStarted 归到当前活动片段时。它像门口核对票号的人,避免把不同轮次的元数据混到一起。

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

finalize_active_segment45–91 ↗
fn finalize_active_segment(
    active_segment: ActiveReplaySegment<'a>,
    base_replacement_history: &mut Option<&'a [ResponseItem]>,
    previous_turn_settings: &mut Option<PreviousTurnSettings>,

作用:这个函数负责把刚刚倒序收集好的一段对话“结账”。它决定这段是否被回滚丢掉,是否提供了最新可用的历史存档点、上一轮设置、参考上下文和窗口编号。

数据流:输入是一段 ActiveReplaySegment,以及几个外部正在累计的结果槽位:基础替换历史、上一轮设置、参考上下文、窗口编号、还要回滚几轮。它先看是否还有待回滚的用户轮次:如果这段算用户轮次,就消耗一个回滚名额并直接丢弃。若没有被丢弃,它会把这段里最新且还没被填过的信息写进那些结果槽位。输出不是返回值,而是修改这些传入的槽位,让后续重建历史时知道该从哪里开始、该用哪些恢复信息。

调用关系:Session::reconstruct_history_from_rollout 在倒序扫描时,遇到一轮的开头 TurnStarted,或者扫描结束还有未结算片段时,会调用它。它是倒序扫描阶段的收尾工,把临时片段变成真正会影响恢复结果的信息。

调用图:被 1 处调用(reconstruct_history_from_rollout);外部调用 1 个(matches!)。

Session::reconstruct_history_from_rollout94–335 ↗
async fn reconstruct_history_from_rollout(
        &self,
        turn_context: &TurnContext,
        rollout_items: &[RolloutItem],
    ) -> RolloutReconstruction

作用:这是本文件的主函数:给它当前 turn_context 和一串 rollout_items,它会重建出会话历史,并顺手算出恢复或分叉会话需要的元数据。有人恢复旧会话时,核心就靠它判断哪些记录还有效、哪些已经被压缩或回滚。

数据流:输入是当前会话的 TurnContext,以及按时间保存的 RolloutItem 列表。它先从最新往最旧扫描,找最近仍然有效的历史存档点、上一轮模型设置、参考上下文和窗口编号;扫描中会识别回滚事件,并用 turn_ids_are_compatible 防止轮次混淆,用 finalize_active_segment 结算每个倒序片段。找到足够信息后,它再从存档点之后的记录正向重放:普通响应写入 ContextManager,跨代理通信转成模型输入再写入,压缩记录会替换历史,回滚记录会删除最近的用户轮次。最后输出 RolloutReconstruction,里面有最终 history、previous_turn_settings、reference_context_item 和 window_id。

调用关系:它是整个重建流程的入口。它调用 turn_ids_are_compatible 来判断记录归属,调用 finalize_active_segment 来完成每个片段的倒序结算;正向重放时,它借助 ContextManager::new 之类的上下文工具维护历史,还会在旧式压缩记录上调用 collect_user_messages 和 build_compacted_history 来重新生成压缩后的历史。

调用图:调用 5 个内部函数(build_compacted_history, collect_user_messages, new, finalize_active_segment, turn_ids_are_compatible);外部调用 10 个(new, default, new, Latest, is_user_turn_boundary, matches!, iter, once, try_from, try_from)。

core/src/session_rollout_init_error.rs源码 ↗
domain_logicstartup / session initialization

Codex 启动或创建会话时,需要在自己的主目录下面保存会话文件。这个过程会碰到很多很现实的问题:比如目录没权限、目录不存在、该是文件夹的地方却是普通文件、旧数据坏了。这个文件就像一个“错误翻译员”:它先从一串错误原因里找出真正的磁盘读写错误,也就是 std::io::Error(操作系统返回的文件访问错误);如果能认出常见情况,就给出明确建议,例如修权限、建目录、清理损坏会话。它最终返回 CodexErr::Fatal,意思是这是一个不能继续运行的严重错误,但提示会尽量告诉人该怎么修。如果不是它认识的文件错误,就保留原始错误链,给出通用的“初始化会话失败”。

函数细节2
map_session_init_error7–17 ↗
fn map_session_init_error(err: &anyhow::Error, codex_home: &Path) -> CodexErr

作用:把一次会话初始化失败,转换成 Codex 对外显示的严重错误。它的重点不是简单报错,而是尽量从复杂错误里挖出真正原因,并给用户一个能照着做的修复提示。

数据流:进去的是一个 anyhow::Error(可能包着很多层原因的错误)和 Codex 主目录路径 → 它沿着错误链一层层查看,寻找其中的 std::io::Error(文件或目录访问错误),再尝试交给 map_rollout_io_error 翻译 → 如果翻译成功,就出来一个带具体建议的 CodexErr::Fatal;如果没有识别到,就出来一个通用的“Failed to initialize session”严重错误,不改动任何文件。

调用关系:它是这个文件对外的入口。会话初始化流程失败时会调用它来整理错误。它自己不判断所有细节,而是把找到的底层文件错误交给 map_rollout_io_error;如果对方认不出来,它才兜底生成普通的 Fatal 错误。

调用图:外部调用 3 个(chain, format!, Fatal)。

map_rollout_io_error19–49 ↗
fn map_rollout_io_error(io_err: &std::io::Error, codex_home: &Path) -> Option<CodexErr>

作用:把具体的文件系统错误,变成更像人话的故障说明和修复建议。比如看到“PermissionDenied”时,它会说明 Codex 不能访问会话文件,并提示可能需要修正目录所有者。

数据流:进去的是一个 std::io::Error(操作系统给出的文件错误)和 Codex 主目录路径 → 它先拼出会话目录路径,也就是 codex_home 下面的 sessions 子目录,然后根据错误种类分别写提示:权限不足、目录缺失、已有文件挡住、数据损坏、路径类型不对等 → 如果是认识的情况,就出来 Some(CodexErr::Fatal),里面带有人能读懂的提示和原始底层错误;如果是不认识的错误种类,就出来 None,让上层用通用错误处理。

调用关系:它是 map_session_init_error 的具体翻译工具。上层先负责从复杂错误链里找出 io 错误,再把这个 io 错误交给它。它不负责决定整个初始化是否失败,只负责判断“这个文件错误能不能给出更好的说明”。

调用图:外部调用 4 个(join, kind, format!, Fatal)。

external-agent-sessions/src/export.rs源码 ↗
domain_logic导入外部会话时运行;测试函数只在测试阶段运行

可以把这个文件想成一个“聊天记录转接头”。外部导入文件里通常只有一串用户和助手的消息,还有当前工作目录、标题等信息;但本项目内部需要的是一组 RolloutItem,也就是带有事件顺序的会话流水账。这里先读取导入文件,确认它有工作目录和有效消息,再找标题:优先用来源给的标题,没有就用第一条用户消息做一个短标题。之后它按用户消息切分成一轮一轮对话:用户发话时开启新回合,助手回复时追加到当前回合。最后还会加一个“外部会话已导入”的标记,并估算 token(大致可理解为模型看到的文字量)使用量,让界面和后续逻辑知道这段历史是导入来的,不是当前系统刚生成的。文件底部的测试用临时 JSONL 文件反复验证这些规则没有走样。

函数细节19
load_session_for_import24–29 ↗
fn load_session_for_import(path: &Path) -> io::Result<Option<ImportedExternalAgentSession>>

作用:这是测试里用的简化入口:读取一个外部会话文件,但只关心导入后的会话本身,不关心文件内容的哈希值。哈希值可以理解为文件内容的指纹。

数据流:输入是一个文件路径 → 它调用完整版本的导入函数拿到“会话加内容指纹” → 把内容指纹丢掉,只返回可导入的会话,或者返回没有可导入内容。

调用关系:多个测试函数用它来模拟真实导入流程;它自己不解析文件,而是把工作交给 load_session_for_import_with_content_sha256。

调用图:调用 1 个内部函数(load_session_for_import_with_content_sha256);被 7 处调用(adds_import_marker_without_copying_last_agent_message, builds_visible_turns_for_imported_history, emits_token_usage_for_imported_history, loads_ai_title_for_imported_session, loads_custom_title_for_imported_session, loads_custom_title_over_later_ai_title_for_imported_session, stores_imported_messages_as_response_items_and_visible_events)。

load_session_for_import_with_content_sha25631–57 ↗
fn load_session_for_import_with_content_sha256(
    path: &Path,
) -> io::Result<Option<(ImportedExternalAgentSession, String)>>

作用:这是外部会话导入的主要入口。它读取外部聊天记录,检查是否够格导入,并转换成本项目内部的会话对象,同时保留文件内容指纹,方便之后判断文件有没有变。

数据流:输入是外部会话文件路径 → 它先用 read_session_import 读出工作目录、消息、标题和内容指纹 → 如果没有工作目录或没有可转换的聊天流水,就返回空 → 否则生成标题、第一条用户消息摘要和内部 RolloutItem 列表 → 输出 ImportedExternalAgentSession 加内容 SHA-256 指纹。

调用关系:真实导入流程中的 load_importable_session 会调用它,测试辅助函数 load_session_for_import 也会调用它;它把“原始消息变成内部流水账”的核心工作交给 rollout_items_from_messages。

调用图:调用 2 个内部函数(rollout_items_from_messages, read_session_import);被 2 处调用(load_session_for_import, load_importable_session)。

rollout_items_from_messages59–121 ↗
fn rollout_items_from_messages(messages: Vec<ConversationMessage>) -> Vec<RolloutItem>

作用:这个函数把一串普通聊天消息,改造成系统能理解的“回合流水账”。没有它,界面就不知道哪条消息属于哪一轮,也不知道导入历史在哪里结束。

数据流:输入是一组 ConversationMessage → 它从头到尾扫描,遇到用户消息就开启新回合,必要时先结束上一回合;遇到助手消息就放进当前回合;同时把消息保存成可回放的 ResponseItem,并按字节数粗略估算 token → 最后补上“外部会话已导入”标记、token 统计和回合结束事件 → 输出 RolloutItem 列表。

调用关系:load_session_for_import_with_content_sha256 调用它完成最关键的格式转换;它内部会请 external_session_imported_marker_item 造导入标记,请 response_item 造消息记录,请 message_byte_count 和 approx_tokens_from_byte_count_i64 估算文字量,请 token_count_item 和 turn_complete_item 补齐系统事件。

调用图:调用 5 个内部函数(external_session_imported_marker_item, message_byte_count, response_item, token_count_item, turn_complete_item);被 1 处调用(load_session_for_import_with_content_sha256);外部调用 9 个(default, new, approx_tokens_from_byte_count_i64, format!, AgentMessage, TurnStarted, UserMessage, EventMsg, ResponseItem)。

external_session_imported_marker_item123–129 ↗
fn external_session_imported_marker_item() -> RolloutItem

作用:这个函数生成一条特殊的助手消息,内容是“<EXTERNAL SESSION IMPORTED>”。它像文件夹里的分隔纸,提醒系统和界面:前面的内容是从外部搬进来的历史。

数据流:它不需要输入 → 直接创建一个 AgentMessageEvent,也就是助手可见消息事件 → 输出包装好的 RolloutItem。

调用关系:rollout_items_from_messages 在导入历史的最后一轮结束前调用它,用来给导入内容打标记;测试会检查这个标记确实出现在界面可见的回合里。

调用图:被 1 处调用(rollout_items_from_messages);外部调用 2 个(AgentMessage, EventMsg)。

response_item131–146 ↗
fn response_item(message: ConversationMessage) -> ResponseItem

作用:这个函数把一条导入来的聊天消息,包装成本项目内部保存模型响应时使用的消息格式。它保证用户消息和助手消息都能被后续回放或展示。

数据流:输入是一条 ConversationMessage → 如果角色是助手,就把文本放进输出文本;如果角色是用户,就把文本放进输入文本;同时把角色写成字符串 user 或 assistant → 输出一个 ResponseItem::Message。

调用关系:rollout_items_from_messages 每处理一条有效消息都会调用它;它和可见事件配合,让同一条导入消息既能显示给人看,也能作为内部历史被保存。

调用图:被 1 处调用(rollout_items_from_messages);外部调用 1 个(vec!)。

message_byte_count148–150 ↗
fn message_byte_count(message: &ConversationMessage) -> i64

作用:这个函数计算一条消息文本占多少字节,用来粗略估算 token 数。token 可以简单理解成模型处理文字时的“计量单位”。

数据流:输入是一条消息引用 → 它读取消息文本长度,并安全转换成 64 位整数;如果极端情况下长度太大无法转换,就用最大值兜底 → 输出字节数。

调用关系:rollout_items_from_messages 用它累计导入消息的大小,然后再交给 approx_tokens_from_byte_count_i64 估算模型可见 token 数。

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

token_count_item152–165 ↗
fn token_count_item(last_model_visible_tokens: i64) -> RolloutItem

作用:这个函数生成一条 token 使用量事件,让系统知道导入历史大概占用了多少模型上下文空间。上下文空间可以理解成模型一次能“记住并参考”的文字容量。

数据流:输入是估算出来的 token 数 → 它填到 TokenUsage 里,并同时作为总用量和最近一次用量 → 输出一个 TokenCount 类型的 RolloutItem。

调用关系:rollout_items_from_messages 在最后补齐导入历史信息时调用它;测试 emits_token_usage_for_imported_history 会确认这条统计事件被生成并且数值大于零。

调用图:被 1 处调用(rollout_items_from_messages);外部调用 3 个(TokenCount, EventMsg, default)。

turn_complete_item167–175 ↗
fn turn_complete_item(turn_id: String, completed_at: Option<i64>) -> RolloutItem

作用:这个函数生成“某一轮对话结束了”的事件。它告诉系统一个用户回合已经收尾,后面可以开始新回合或完成导入。

数据流:输入是回合 ID 和可选的完成时间 → 它创建 TurnCompleteEvent,并明确不附带 last_agent_message → 输出一个 TurnComplete 类型的 RolloutItem。

调用关系:rollout_items_from_messages 在遇到新用户消息时会用它结束上一轮,在所有消息处理完后也会用它结束最后一轮;相关测试会确认它不会把最后一条助手消息重复塞进去。

调用图:被 1 处调用(rollout_items_from_messages);外部调用 2 个(TurnComplete, EventMsg)。

tests::builds_visible_turns_for_imported_history187–219 ↗
fn builds_visible_turns_for_imported_history()

作用:这个测试确认导入的聊天历史能被正确分成界面上看得见的多轮对话。它重点检查两次用户请求会变成两个回合。

数据流:它创建临时目录和临时 JSONL 会话文件 → 写入用户、助手、用户三条记录 → 调用 load_session_for_import 导入,再用 build_turns_from_rollout_items 生成界面回合 → 断言回合数量、每轮条目数量和导入标记都符合预期。

调用关系:它站在用户界面的角度验证 rollout_items_from_messages 的结果;它依赖 record 和 jsonl 生成测试输入,并通过 load_session_for_import 进入正式导入逻辑。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 7 个(new, assert_eq!, build_turns_from_rollout_items, jsonl, record, create_dir_all, write)。

tests::adds_import_marker_without_copying_last_agent_message222–263 ↗
fn adds_import_marker_without_copying_last_agent_message()

作用:这个测试确认导入结束时会加上导入标记,但不会把最后一条助手回复错误地复制到回合结束事件里。这样可以避免界面或历史记录里出现重复内容。

数据流:它写入一轮用户和助手消息 → 导入后构建可见回合 → 检查最后一个可见条目是导入标记 → 再倒着查找最后的 TurnComplete 事件,确认其中没有 last_agent_message。

调用关系:它验证 external_session_imported_marker_item 和 turn_complete_item 配合得是否正确;入口仍然是 load_session_for_import。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 7 个(new, assert_eq!, build_turns_from_rollout_items, jsonl, record, create_dir_all, write)。

tests::stores_imported_messages_as_response_items_and_visible_events266–307 ↗
fn stores_imported_messages_as_response_items_and_visible_events()

作用:这个测试确认导入消息会同时以两种形式保存:一种给系统内部回放用,一种给界面显示用。少了任何一种,后续使用都会不完整。

数据流:它生成较长的用户请求和助手回答 → 写入临时文件并导入 → 在导入结果里数 ResponseItem::Message 的数量,也数可见的用户/助手事件数量 → 断言两类记录都各自完整存在。

调用关系:它主要验证 rollout_items_from_messages 对 response_item 和可见事件的双重写入;它用 record 和 jsonl 准备输入,用 load_session_for_import 跑完整转换。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 6 个(new, assert_eq!, jsonl, record, create_dir_all, write)。

tests::loads_custom_title_for_imported_session310–329 ↗
fn loads_custom_title_for_imported_session()

作用:这个测试确认如果外部文件提供了用户自定义标题,导入后的会话会使用这个标题。标题就像聊天列表里的名字,能帮助人快速识别会话。

数据流:它写入一条用户消息和一条 custom-title 记录 → 导入会话 → 读取 imported.title → 断言标题等于外部文件给出的自定义标题。

调用关系:它验证 load_session_for_import_with_content_sha256 里选择标题的规则;custom_title_record 负责生成测试用标题记录。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 7 个(new, assert_eq!, custom_title_record, jsonl, record, create_dir_all, write)。

tests::loads_ai_title_for_imported_session332–351 ↗
fn loads_ai_title_for_imported_session()

作用:这个测试确认如果外部文件提供的是 AI 生成标题,系统也能把它当作导入会话标题使用。

数据流:它写入一条用户消息和一条 ai-title 记录 → 调用导入函数 → 检查导入结果里的 title → 输出结果是测试通过或失败。

调用关系:它通过 load_session_for_import 触发真实导入逻辑,ai_title_record 负责制造测试输入;它覆盖的是标题读取的另一个来源。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 7 个(new, assert_eq!, ai_title_record, jsonl, record, create_dir_all, write)。

tests::loads_custom_title_over_later_ai_title_for_imported_session354–374 ↗
fn loads_custom_title_over_later_ai_title_for_imported_session()

作用:这个测试确认当自定义标题和 AI 标题都存在时,自定义标题优先。也就是说,人起的名字比机器生成的名字更重要。

数据流:它写入用户消息、自定义标题、再写入 AI 标题 → 导入会话 → 检查最终 title → 断言结果仍然是自定义标题,而不是后面的 AI 标题。

调用关系:它验证 read_session_import 和 load_session_for_import_with_content_sha256 合起来体现出的标题优先级;输入由 custom_title_record、ai_title_record、record 和 jsonl 共同构造。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 8 个(new, assert_eq!, ai_title_record, custom_title_record, jsonl, record, create_dir_all, write)。

tests::emits_token_usage_for_imported_history377–406 ↗
fn emits_token_usage_for_imported_history()

作用:这个测试确认导入历史会生成 token 用量信息。这样系统后面判断上下文长度时,不会把导入内容当成不存在。

数据流:它写入三条聊天记录 → 导入成 RolloutItem 列表 → 从里面找到 TokenCount 事件 → 检查最近用量大于零,并且总用量和最近用量一致。

调用关系:它验证 rollout_items_from_messages 最后调用 token_count_item 的效果;测试输入仍由 record 和 jsonl 创建,导入入口是 load_session_for_import。

调用图:调用 1 个内部函数(load_session_for_import);外部调用 7 个(new, assert!, assert_eq!, jsonl, record, create_dir_all, write)。

tests::record408–416 ↗
fn record(role: &str, text: &str, cwd: &Path) -> JsonValue

作用:这是测试用的小工具,用来造一条普通聊天记录 JSON。它让每个测试不用重复手写同样的字段。

数据流:输入是角色、文本和工作目录 → 它取当前 UTC 时间,组装 type、cwd、timestamp 和 message.content 字段 → 输出一个 JSON 值。

调用关系:多个测试用它生成 user 或 assistant 记录,再交给 jsonl 拼成临时会话文件;它只服务测试,不参与正式运行。

调用图:外部调用 2 个(now, json!)。

tests::custom_title_record418–423 ↗
fn custom_title_record(title: &str) -> JsonValue

作用:这是测试用的小工具,用来生成一条“自定义标题”记录。它专门帮助测试标题导入规则。

数据流:输入是标题文本 → 它包装成 type 为 custom-title、字段为 customTitle 的 JSON → 输出这个 JSON 值。

调用关系:标题相关测试调用它,然后由 jsonl 写入测试文件;正式导入逻辑读取后应把它识别为来源标题。

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

tests::ai_title_record425–430 ↗
fn ai_title_record(title: &str) -> JsonValue

作用:这是测试用的小工具,用来生成一条“AI 标题”记录。它帮助测试系统能不能识别机器生成的会话标题。

数据流:输入是标题文本 → 它包装成 type 为 ai-title、字段为 aiTitle 的 JSON → 输出这个 JSON 值。

调用关系:AI 标题相关测试调用它,再通过 jsonl 放入临时文件;导入函数随后读取并应用标题规则。

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

tests::jsonl432–438 ↗
fn jsonl(records: &[JsonValue]) -> String

作用:这个测试工具把多条 JSON 记录拼成 JSONL 文本。JSONL 就是一行一个 JSON,常用来保存日志式数据。

数据流:输入是一组 JSON 值 → 它把每个 JSON 转成字符串 → 用换行符连接 → 输出可以直接写入测试会话文件的文本。

调用关系:几乎所有测试都会用它把 record、custom_title_record 或 ai_title_record 生成的记录写成文件;之后 load_session_for_import 再读取这个文件。

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

rollout/src/policy.rs源码 ↗
domain_logiccross-cutting;在追加 rollout 记录、保存回放材料、筛选记忆内容时会用到

rollout 文件可以理解成一份“运行流水账”:系统和用户说了什么、工具调用了什么、某些关键阶段发生了什么。如果什么都记,文件会又大又乱,还可能保存一些没必要的过程噪音;如果记少了,以后想回放一次运行、分析问题或恢复上下文,又会缺关键线索。这个文件就是专门定规则的地方。它会检查三类东西:模型返回的 ResponseItem(一次回答里的消息、推理、工具调用等)、系统事件 EventMsg(用户消息、回合开始结束、搜索完成等)、以及 rollout 自己的包装项 RolloutItem。主要入口是 is_persisted_rollout_item,它先看外层是什么,再交给更细的判断函数。persisted_rollout_items 则把一批记录过滤成“真正该写盘”的新列表。还有一个单独给 memories(长期记忆)用的规则:比如 developer 角色的消息不会进入记忆,避免把开发者指令当成普通记忆保存。

函数细节5
is_persisted_rollout_item6–16 ↗
fn is_persisted_rollout_item(item: &RolloutItem) -> bool

作用:判断单个 rollout 项目要不要保存到 rollout 文件里。它是总入口,会先看这个项目属于哪一大类,再用对应的小规则来决定。

数据流:进去的是一个 RolloutItem,也就是一条可能要写入流水账的记录。函数先拆开看:如果里面是 ResponseItem,就交给 should_persist_response_item;如果是 EventMsg,就交给 should_persist_event_msg;如果是协作通信、压缩标记、回合上下文或会话元信息,就直接认为要保存。出来的是 true 或 false,表示这条记录是否该留下。

调用关系:persisted_rollout_items 在过滤一整批记录时会逐条调用它。它自己不做所有细节判断,而是把模型回答交给 should_persist_response_item,把系统事件交给 should_persist_event_msg,像前台分诊一样把不同病人送到不同窗口。

调用图:调用 2 个内部函数(should_persist_event_msg, should_persist_response_item);被 1 处调用(persisted_rollout_items)。

persisted_rollout_items19–27 ↗
fn persisted_rollout_items(items: &[RolloutItem]) -> Vec<RolloutItem>

作用:从一批 rollout 项目里挑出应该保存的那些,生成一个干净的新列表。有人要把实时产生的记录追加到文件前,会用它先过一遍筛子。

数据流:进去的是 RolloutItem 的列表切片,也就是一批候选记录。函数先创建一个空列表,然后逐条检查:每条都交给 is_persisted_rollout_item 判断;通过的就复制一份放进新列表,没通过的丢掉。出来的是只包含应保存项目的 Vec<RolloutItem>,原来的输入列表不会被改动。

调用关系:它是批量过滤的外层工具,会在“实时追加记录”这类场景里使用。它把核心判断交给 is_persisted_rollout_item,自己只负责循环、收集结果,并用 Vec::new 创建输出容器。

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

should_persist_response_item31–50 ↗
fn should_persist_response_item(item: &ResponseItem) -> bool

作用:判断模型返回内容里的某一项 ResponseItem 要不要写进 rollout 文件。它保留能还原对话和工具行为的主要内容,跳过不值得长期保存的触发器或未知杂项。

数据流:进去的是一个 ResponseItem,比如普通消息、智能体消息、推理内容、本地命令调用、函数调用、搜索结果、图片生成、上下文压缩等。函数用类型匹配逐项判断:大多数真实内容和工具调用会返回 true;CompactionTrigger 这种只是触发压缩的提示,以及 Other 这种无法明确归类的内容会返回 false。出来的是一个布尔值,告诉上层是否保存。

调用关系:is_persisted_rollout_item 遇到 RolloutItem::ResponseItem 时会调用它。它不负责写文件,也不处理事件,只专心回答一个问题:这条模型响应内容是不是值得进入 rollout 流水账。

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

should_persist_response_item_for_memories54–73 ↗
fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool

作用:判断某个 ResponseItem 是否适合进入 memories,也就是系统的长期记忆材料。它的标准比 rollout 保存更谨慎,因为记忆会影响以后,而不仅是用于回放。

数据流:进去的是一个 ResponseItem。函数会看它是什么类型:普通 Message 会进一步检查角色,如果 role 是 developer 就不保存,因为这类内容通常是开发者指令,不该混进记忆;工具调用和工具输出、网页搜索等可观察行为会保存;智能体内部消息、推理、图片生成、压缩相关内容和未知项不会保存。出来的是 true 或 false,只表示是否适合做记忆材料,不会改动输入。

调用关系:这个函数没有出现在给出的调用关系里,说明它可能是给别的模块按需调用的独立规则。它和 should_persist_response_item 很像,但服务目标不同:一个面向 rollout 回放流水账,一个面向长期记忆,因此取舍也不同。

should_persist_event_msg77–165 ↗
fn should_persist_event_msg(ev: &EventMsg) -> bool

作用:判断一条系统事件 EventMsg 要不要写进 rollout 文件。它负责把关键节点留下,比如用户消息、回合开始结束、搜索完成、上下文压缩完成等,同时过滤掉大量临时进度和细碎通知。

数据流:进去的是一个 EventMsg,也就是运行过程中产生的一条事件。函数按事件种类逐个判断:能帮助以后回放或分析流程的事件返回 true;只是开始通知、实时增量、审批请求、终端输出片段、启动进度、警告等临时信息大多返回 false。特殊的是 ItemCompleted:它只在完成的项目是 Plan 或 Sleep 时保存,因为这些项目没有等价的原始 ResponseItem 或旧事件可替代。出来的是一个布尔值,告诉上层这条事件是否该长期留在 rollout 文件里。

调用关系:is_persisted_rollout_item 遇到 RolloutItem::EventMsg 时会调用它。它内部用 matches! 这个 Rust 匹配工具来处理 ItemCompleted 的特殊判断。整体上,它是事件保存规则的集中清单,保证文件里留下的是关键剧情,而不是每一帧动画。

调用图:被 1 处调用(is_persisted_rollout_item);外部调用 1 个(matches!)。

rollout/src/metadata.rs源码 ↗
orchestrationstartup/background backfill, rollout metadata extraction

项目里的会话会先被写成 rollout 文件,可以理解成一份聊天过程流水账。数据库里需要的是更方便检索的摘要信息,比如会话 ID、创建时间、工作目录、模型、Git 分支、是否归档等。这个文件就是“从流水账提炼登记表”的地方。它优先读取文件里的 SessionMeta,也就是会话开头写下的正式资料;如果老文件没有这段资料,就从文件名里猜出创建时间和会话 ID,尽量不让旧数据丢失。它还负责 backfill(补录):递归扫描正常会话目录和归档会话目录,逐个读取文件、提取元数据、写入状态数据库,并记录进度水位线,避免下次从头再扫。为了防止多个任务同时补录,它会先抢一个租约,像拿到“施工许可证”后才开工。

函数细节9
builder_from_session_meta37–62 ↗
fn builder_from_session_meta(
    session_meta: &SessionMetaLine,
    rollout_path: &Path,
) -> Option<ThreadMetadataBuilder>

作用:把会话文件里的 SessionMeta 行转换成一个元数据构建器。构建器可以理解成一张还没正式提交的登记表草稿。

数据流:进去的是一条 SessionMeta 和这个 rollout 文件的路径;它先把文本时间解析成 UTC 时间,再把会话 ID、来源、模型供应商、昵称、角色、工作目录、CLI 版本、Git 信息等填进 ThreadMetadataBuilder;出来的是一个可继续补充、最后生成正式元数据的构建器。如果时间格式看不懂,就返回空,表示这条资料不能用。

调用关系:它是 builder_from_items 的优先路线:只要会话文件里有正式的 SessionMeta,builder_from_items 就会把工作交给它,而不是再从文件名里猜信息。它内部会调用 parse_timestamp_to_utc 来把字符串时间变成机器能比较的时间。

调用图:调用 2 个内部函数(parse_timestamp_to_utc, new);被 1 处调用(builder_from_items);外部调用 2 个(to_path_buf, new_read_only_policy)。

builder_from_items64–92 ↗
fn builder_from_items(
    items: &[RolloutItem],
    rollout_path: &Path,
) -> Option<ThreadMetadataBuilder>

作用:从一整个会话文件的记录项里找出能建立元数据的基础信息。它兼容新旧文件:新文件读正式元数据,旧文件就退回去读文件名。

数据流:进去的是 rollout 文件里解析出的多条 RolloutItem 和文件路径;它先在这些记录里寻找 SessionMeta,找到就交给 builder_from_session_meta;如果找不到,就从文件名拆出时间戳和 UUID,再创建一个最基础的 ThreadMetadataBuilder;出来的是构建器,或者在信息实在不够时返回空。

调用关系:extract_metadata_from_rollout 会靠它先搭好元数据骨架,然后再逐条应用会话事件补充细节。调用图里也显示 apply_rollout_items 会用它,说明它是“从原始记录进入元数据世界”的共同入口。

调用图:调用 4 个内部函数(from_string, parse_timestamp_uuid_from_filename, builder_from_session_meta, new);被 2 处调用(extract_metadata_from_rollout, apply_rollout_items);外部调用 6 个(from_timestamp, file_name, to_path_buf, default, parse_rollout_file_name, iter)。

extract_metadata_from_rollout94–131 ↗
async fn extract_metadata_from_rollout(
    rollout_path: &Path,
    default_provider: &str,
) -> anyhow::Result<ExtractionOutcome>

作用:读取一个 rollout 会话文件,并提炼出完整的会话元数据。有人想把单个会话导入数据库、修复索引或做恢复时,会用到它。

数据流:进去的是一个 rollout 文件路径和默认模型供应商名称;它先用 RolloutRecorder::load_rollout_items 读出文件里的所有记录项和解析错误数;如果文件为空就报错;然后用 builder_from_items 建立元数据骨架,再把每条记录交给 apply_rollout_item 逐步更新标题、时间、状态等信息;最后用文件修改时间补上 updated_at。出来的是 ExtractionOutcome,里面有元数据、memory_mode(记忆功能模式)和解析错误数量。

调用关系:backfill_sessions_with_lease 在批量补录时会反复调用它。其他流程,比如按当前目录寻找可恢复会话、重新对齐 rollout 和数据库,也会用它。它把读文件的活交给 RolloutRecorder,把每条记录如何影响元数据的细节交给 codex_state::apply_rollout_item。

调用图:调用 3 个内部函数(builder_from_items, file_modified_time_utc, load_rollout_items);被 4 处调用(backfill_sessions_with_lease, resume_candidate_matches_cwd, reconcile_rollout, reconcile_rollout_preserves_existing_explicit_title);外部调用 2 个(anyhow!, apply_rollout_item)。

backfill_sessions133–145 ↗
async fn backfill_sessions(
    runtime: &codex_state::StateRuntime,
    codex_home: &Path,
    default_provider: &str,
)

作用:这是补录会话元数据的简化入口。调用者不用关心租约时间,它会使用默认的补录租约设置。

数据流:进去的是状态数据库运行时、Codex 主目录和默认模型供应商;它不自己扫描文件,而是把这些参数连同默认租约秒数传给 backfill_sessions_with_lease;出来没有返回值,实际结果是数据库被补齐,日志和指标被记录。

调用关系:wait_for_backfill_gate 会调用它,说明系统会在某个启动或等待阶段触发补录。它只是薄薄的一层包装,真正复杂的扫描、提取和写库都在 backfill_sessions_with_lease 里完成。

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

backfill_sessions_with_lease147–350 ↗
async fn backfill_sessions_with_lease(
    runtime: &codex_state::StateRuntime,
    codex_home: &Path,
    default_provider: &str,
    backfill_lease_seconds: i64,
)

作用:批量扫描旧会话文件,把它们的元数据补进状态数据库。它还会防止重复开工、记录进度、统计成功失败数量。

数据流:进去的是状态数据库运行时、Codex 主目录、默认模型供应商和租约秒数;它先读取补录状态,如果已经完成就直接退出;然后尝试抢占补录租约,抢不到说明别的任务正在做;抢到后扫描 sessions 和 archived sessions 目录,收集 rollout 文件路径,按水位线排序并跳过已经处理过的文件。之后它分批读取每个文件,用 extract_metadata_from_rollout 提取元数据,规范化工作目录,保留数据库里已有的 Git 信息或人工标题,补上归档时间,写入数据库,并恢复 memory_mode。每批结束会 checkpoint(保存进度点),最后标记补录完成并上报指标。

调用关系:backfill_sessions 会把默认参数转交给它,wait_for_backfill_gate 也可能直接调用它。它把找文件交给 collect_rollout_paths,把单文件提取交给 extract_metadata_from_rollout,把文件修改时间交给 file_modified_time_utc,把工作目录清洗交给 normalize_cwd_for_state_db。它是这个文件里最像“总调度员”的函数。

调用图:调用 5 个内部函数(collect_rollout_paths, extract_metadata_from_rollout, file_modified_time_utc, normalize_cwd_for_state_db, default);被 2 处调用(backfill_sessions, wait_for_backfill_gate);外部调用 15 个(default, join, new, global, info!, checkpoint_backfill, get_backfill_state, get_thread, mark_backfill_complete, mark_backfill_running (+5 more))。

backfill_watermark_for_path359–364 ↗
fn backfill_watermark_for_path(codex_home: &Path, path: &Path) -> String

作用:为一个 rollout 文件生成补录进度用的水位线。水位线就是“我已经处理到哪里了”的书签。

数据流:进去的是 Codex 主目录和某个文件路径;它尽量把文件路径变成相对于主目录的路径,并把 Windows 的反斜杠改成统一的斜杠;出来的是一个字符串水位线。它不改文件,也不访问数据库。

调用关系:backfill_sessions_with_lease 在收集到文件后会用它给每个文件做排序和进度标记。这样补录中断后,下次能从上次的书签后面继续,而不是把所有文件重做一遍。

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

file_modified_time_utc366–369 ↗
async fn file_modified_time_utc(path: &Path) -> Option<DateTime<Utc>>

作用:读取文件的最后修改时间,并转换成 UTC 时间。UTC 可以理解成统一时区,方便不同机器、不同地区比较时间。

数据流:进去的是文件路径;它调用 compression::file_modified_time 读取底层文件修改时间,如果读不到就返回空;读到后把秒和纳秒转换成 DateTime<Utc>;出来的是可保存到元数据里的更新时间。

调用关系:extract_metadata_from_rollout 用它给单个会话补 updated_at。backfill_sessions_with_lease 在处理归档文件时也用它推断 archived_at。它是“从文件系统时间补足元数据”的小工具。

调用图:调用 1 个内部函数(file_modified_time);被 2 处调用(backfill_sessions_with_lease, extract_metadata_from_rollout);外部调用 1 个(from_timestamp)。

parse_timestamp_to_utc371–381 ↗
fn parse_timestamp_to_utc(ts: &str) -> Option<DateTime<Utc>>

作用:把文本形式的时间戳转成 UTC 时间。它专门兼容两种常见写法,避免因为格式差异导致会话元数据读不出来。

数据流:进去的是一个时间字符串;它先尝试解析文件名风格的格式,比如 年-月-日T时-分-秒;如果不行,再尝试 RFC3339 格式,也就是常见的带时区标准时间格式;成功后出来一个 UTC 时间,失败就返回空。

调用关系:builder_from_session_meta 在读取 SessionMeta 的创建时间时会调用它。也就是说,只要这里不认识某种时间格式,上层就无法从这条 SessionMeta 建立元数据。

调用图:被 1 处调用(builder_from_session_meta);外部调用 3 个(from_naive_utc_and_offset, parse_from_rfc3339, parse_from_str)。

collect_rollout_paths383–429 ↗
async fn collect_rollout_paths(root: &Path) -> std::io::Result<Vec<PathBuf>>

作用:从一个目录开始往下找出所有 rollout 会话文件。它像一个文件夹巡检员,会进入子目录,但只收集真正能识别的会话文件。

数据流:进去的是根目录路径;它用一个栈保存还要检查的目录,逐个读取目录项;遇到子目录就继续深入,遇到普通文件就让 compression::RolloutFile::from_path 判断是不是 rollout 文件;符合条件的路径会加入结果列表。读目录、读文件类型出错时,它会写警告并跳过,不会让整个扫描直接崩掉。出来的是找到的 rollout 文件路径列表。

调用关系:backfill_sessions_with_lease 会分别对正常会话目录和归档会话目录调用它。它只负责“把候选文件找齐”,后面的排序、过滤水位线、读取内容和写数据库,都由 backfill_sessions_with_lease 接着完成。

调用图:调用 1 个内部函数(from_path);被 1 处调用(backfill_sessions_with_lease);外部调用 4 个(new, read_dir, vec!, warn!)。

state/src/extract.rs源码 ↗
domain_logicrollout replay / metadata extraction

这里做的事很像给一大摞聊天流水账做索引卡片。原始记录里有很多种条目:会话信息、每轮上下文、用户消息、token 统计、目标更新等。这个文件逐条查看这些记录,把会影响列表展示和检索的内容写进 ThreadMetadata,也就是线程的元数据。它会小心区分哪些记录真的能改元数据,哪些只是模型回复或压缩内容,不能乱用。比如用户消息会用来生成标题和预览;只有图片没有文字时,会显示“[Image]”;会话信息会带来工作目录、CLI 版本、Git 信息;轮次上下文会带来模型、审批方式和沙箱权限。文件底部的测试用一批典型场景防止这些规则被改坏,尤其是“不该覆盖的字段不要覆盖”。

函数细节22
apply_rollout_item15–31 ↗
fn apply_rollout_item(
    metadata: &mut ThreadMetadata,
    item: &RolloutItem,
    default_provider: &str,
)

作用:这是入口函数:拿到一条对话流水记录,就把它能贡献的信息写进线程元数据。别人不需要关心记录是哪一类,直接交给它即可。

数据流:输入是一份可修改的 ThreadMetadata、一条 RolloutItem,以及默认模型提供商名称。它先看条目类型:会话信息交给 apply_session_meta_from_item,轮次上下文交给 apply_turn_context,事件消息交给 apply_event_msg,回复条目交给 apply_response_item;不会影响元数据的条目就跳过。最后如果模型提供商还是空的,就补上默认值。

调用关系:它是本文件的总分发员。测试里的各种场景都会调用它,实际系统重放聊天记录时也会按条调用它;它再把具体活儿分给几个更小的函数。

调用图:调用 4 个内部函数(apply_event_msg, apply_response_item, apply_session_meta_from_item, apply_turn_context);被 10 处调用(event_msg_blank_user_message_without_images_keeps_first_user_message_empty, event_msg_image_only_user_message_sets_image_placeholder_preview, event_msg_thread_goal_sets_preview_only_and_later_user_sets_message_title, event_msg_user_messages_set_title_and_first_user_message, response_item_user_messages_do_not_set_title_or_first_user_message, session_meta_does_not_set_model_or_reasoning_effort, turn_context_does_not_override_session_cwd, turn_context_sets_cwd_when_session_cwd_missing, turn_context_sets_model_and_reasoning_effort, turn_context_sets_permission_profile_metadata)。

rollout_item_affects_thread_metadata34–45 ↗
fn rollout_item_affects_thread_metadata(item: &RolloutItem) -> bool

作用:这个函数回答一个简单问题:这条流水记录会不会改变存在数据库里的线程元数据。它可以帮助系统少做无用功。

数据流:输入是一条 RolloutItem。它只检查条目的种类,不修改任何数据;如果是会话信息、轮次上下文、token 统计、用户消息或目标更新,就返回 true,否则返回 false。

调用关系:它像一个过滤器,通常会在真正更新元数据前使用。它不调用别的本文件函数,也不负责修改内容,只负责判断值不值得处理。

apply_session_meta_from_item47–73 ↗
fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &SessionMetaLine)

作用:这个函数从“会话开头的基本信息”里提取线程身份、来源、工作目录、CLI 版本和 Git 信息。它保证元数据有一个可靠的基础档案。

数据流:输入是可修改的 ThreadMetadata 和 SessionMetaLine。它先确认记录里的线程 ID 和当前元数据 ID 一致;不一致就直接忽略,避免把别的分支会话的信息混进来。确认后,它把来源、代理名称、角色、路径、模型提供商、CLI 版本、当前目录、Git 提交号、分支和仓库地址写入元数据。

调用关系:它只由 apply_rollout_item 调用,专门处理 RolloutItem::SessionMeta。遇到枚举值时,它会交给 enum_to_string 转成普通字符串,方便存数据库和展示。

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

apply_turn_context75–84 ↗
fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem)

作用:这个函数读取某一轮对话的运行环境,比如当前目录、使用的模型、推理强度、沙箱权限和审批模式。它让线程摘要知道这轮对话是在什么规则下运行的。

数据流:输入是可修改的 ThreadMetadata 和 TurnContextItem。它在元数据还没有工作目录时,才用轮次里的目录补上;然后写入模型名、推理强度、权限配置的 JSON 字符串,以及审批模式字符串。

调用关系:它由 apply_rollout_item 在遇到 RolloutItem::TurnContext 时调用。它会用 permission_profile 取出最终权限配置,用 serde_json 转成字符串,并用 enum_to_string 把审批策略转成易存储的文字。

调用图:调用 2 个内部函数(permission_profile, enum_to_string);被 1 处调用(apply_rollout_item);外部调用 1 个(to_string)。

apply_event_msg86–114 ↗
fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg)

作用:这个函数处理运行过程中发生的事件消息,重点是用户说了什么、token 用了多少、线程目标是什么。它决定标题、预览和第一条用户消息这些最容易被用户看到的字段。

数据流:输入是可修改的 ThreadMetadata 和 EventMsg。token 统计会更新 tokens_used;用户消息会生成预览,必要时设置 first_user_message,并从消息正文生成 title;目标更新会在预览为空时用目标内容补上。其它事件不改元数据。

调用关系:它由 apply_rollout_item 调用。它会请 user_message_preview 生成用户消息摘要,请 strip_user_message_prefix 去掉系统加的消息前缀,请 set_preview_if_empty 避免覆盖已有预览。

调用图:调用 3 个内部函数(set_preview_if_empty, strip_user_message_prefix, user_message_preview);被 1 处调用(apply_rollout_item)。

apply_response_item116–116 ↗
fn apply_response_item(_metadata: &mut ThreadMetadata, _item: &ResponseItem)

作用:这个函数目前故意什么都不做。它保留了一个位置,表示模型回复条目现在不会改变线程元数据。

数据流:输入是可修改的 ThreadMetadata 和 ResponseItem,但两个参数都没有被使用。进去什么,出来还是什么,不产生任何修改。

调用关系:它由 apply_rollout_item 在遇到 RolloutItem::ResponseItem 时调用。测试专门确认,即使 ResponseItem 里看起来像用户消息,也不能用它设置标题或第一条用户消息。

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

set_preview_if_empty118–122 ↗
fn set_preview_if_empty(metadata: &mut ThreadMetadata, preview: Option<String>)

作用:这个小函数只在预览还没有内容时设置预览。它防止后来的消息把更早、更代表性的预览覆盖掉。

数据流:输入是可修改的 ThreadMetadata 和一个可能存在的预览文字。它检查 metadata.preview;如果为空,就写入这段文字;如果已经有值,就保持不变。

调用关系:它由 apply_event_msg 调用。用户消息和线程目标更新都可能想设置预览,但最终谁先写入,谁就保留下来。

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

strip_user_message_prefix124–129 ↗
fn strip_user_message_prefix(text: &str) -> &str

作用:这个函数把用户消息前面系统加的固定标记去掉,只留下真正给人看的内容。没有标记时,它就简单去掉首尾空白。

数据流:输入是一段文本。它查找 USER_MESSAGE_BEGIN 这个固定开头;找到了就从开头后面截取并修剪空白,找不到就直接修剪原文。输出是一段借用原文本的干净字符串切片。

调用关系:它被 apply_event_msg 和 user_message_preview 使用。前者用它生成标题,后者用它生成消息预览。

调用图:被 2 处调用(apply_event_msg, user_message_preview)。

user_message_preview131–145 ↗
fn user_message_preview(user: &UserMessageEvent) -> Option<String>

作用:这个函数给一条用户消息生成列表里能看到的简短预览。文字优先;如果没有文字但有图片,就用“[Image]”提醒用户这是一条图片消息。

数据流:输入是 UserMessageEvent。它先用 strip_user_message_prefix 清理消息文本;如果文本不空,就返回这段文本。如果文本为空,但远程图片或本地图片列表不空,就返回固定占位符“[Image]”。如果什么都没有,就返回 None。

调用关系:它由 apply_event_msg 在处理用户消息时调用。它把“用户到底发了什么”这个判断集中在一处,避免标题和预览规则到处散落。

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

enum_to_string147–153 ↗
fn enum_to_string(value: &T) -> String

作用:这个通用小工具把枚举值转成普通字符串,方便写进数据库或拿来展示。这里的“枚举”可以理解成一组选项中的一个,比如审批模式是 never 还是 on-request。

数据流:输入是任何可以序列化的值。它先用 serde_json 转成 JSON 值;如果结果本来就是字符串,就直接取出字符串;如果是其它 JSON 形态,就转成文字;如果转换失败,就返回空字符串。

调用关系:它被 apply_session_meta_from_item 和 apply_turn_context 使用,也被其它构建或测试代码使用。它把“各种选项怎么变成字符串”的规则统一起来。

调用图:被 4 处调用(apply_session_meta_from_item, apply_turn_context, build, test_thread_metadata);外部调用 2 个(new, to_value)。

tests::response_item_user_messages_do_not_set_title_or_first_user_message185–202 ↗
fn response_item_user_messages_do_not_set_title_or_first_user_message()

作用:这个测试确认:ResponseItem 里的 user 角色消息不能被当成真正的用户输入来设置标题或第一条用户消息。

数据流:它先造一份测试元数据,再造一个看起来像用户消息的 ResponseItem。调用 apply_rollout_item 后,检查 first_user_message、preview 和 title 都没有变化。

调用关系:它通过 apply_rollout_item 走正式流程,间接验证 apply_response_item 目前不改任何元数据。这防止以后有人误把模型回复格式里的内容拿来做线程标题。

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

tests::event_msg_user_messages_set_title_and_first_user_message205–224 ↗
fn event_msg_user_messages_set_title_and_first_user_message()

作用:这个测试确认:真正的用户事件消息会设置第一条用户消息、预览和标题。

数据流:它创建一条带 USER_MESSAGE_BEGIN 前缀的用户消息事件。调用 apply_rollout_item 后,检查三个字段都变成去掉前缀后的“actual user request”。

调用关系:它验证 apply_rollout_item 分发到 apply_event_msg 后,user_message_preview 和 strip_user_message_prefix 能一起把用户输入清理并写入元数据。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 7 个(default, assert_eq!, format!, UserMessage, EventMsg, metadata_for_test, vec!)。

tests::event_msg_image_only_user_message_sets_image_placeholder_preview227–249 ↗
fn event_msg_image_only_user_message_sets_image_placeholder_preview()

作用:这个测试确认:用户只发图片、没发文字时,系统仍然会给线程一个可读预览。

数据流:它造一条消息正文为空但带图片 URL 的用户事件。调用 apply_rollout_item 后,检查 first_user_message 和 preview 都是固定的“[Image]”,而 title 仍然为空。

调用关系:它重点验证 user_message_preview 的图片兜底规则,并确认 apply_event_msg 不会用图片占位符生成标题。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 7 个(default, new, assert_eq!, UserMessage, EventMsg, metadata_for_test, vec!)。

tests::event_msg_blank_user_message_without_images_keeps_first_user_message_empty252–268 ↗
fn event_msg_blank_user_message_without_images_keeps_first_user_message_empty()

作用:这个测试确认:只有空白文字、也没有图片的用户消息,不应该生成假预览。

数据流:它构造一条内容只有空格、图片列表为空的用户事件。调用 apply_rollout_item 后,检查 first_user_message、preview 和 title 都保持为空。

调用关系:它验证 user_message_preview 和 strip_user_message_prefix 对空内容的处理,防止列表里出现没有意义的空标题或空预览。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 6 个(default, assert_eq!, UserMessage, EventMsg, metadata_for_test, vec!)。

tests::event_msg_thread_goal_sets_preview_only_and_later_user_sets_message_title271–312 ↗
fn event_msg_thread_goal_sets_preview_only_and_later_user_sets_message_title()

作用:这个测试确认:线程目标可以先当预览,但不会冒充用户消息或标题;后来的用户消息仍然能设置第一条用户消息和标题。

数据流:它先送入一个目标更新事件,预览变成“optimize the benchmark”,但 first_user_message 和 title 为空。接着送入正常用户消息,预览保持原目标不被覆盖,first_user_message 和 title 则变成新用户消息。

调用关系:它验证 apply_event_msg 中 ThreadGoalUpdated 和 UserMessage 两条路径的配合,也验证 set_preview_if_empty 的“不覆盖已有预览”规则。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 8 个(default, assert_eq!, format!, ThreadGoalUpdated, UserMessage, EventMsg, metadata_for_test, vec!)。

tests::turn_context_does_not_override_session_cwd315–380 ↗
fn turn_context_does_not_override_session_cwd()

作用:这个测试确认:会话信息里已经有工作目录时,后来的轮次上下文不能把它覆盖掉。

数据流:它先把元数据目录清空,再应用一条会话信息,把目录设为 /child/worktree。随后应用一条轮次上下文,里面有另一个目录 /parent/workspace。最后检查目录仍是会话里的值,同时检查权限配置和审批模式被正确写入。

调用关系:它通过 apply_rollout_item 先后触发 apply_session_meta_from_item 和 apply_turn_context,验证两者在 cwd 字段上的优先级。

调用图:调用 2 个内部函数(from_string, apply_rollout_item);外部调用 7 个(from, new, now_v7, assert_eq!, SessionMeta, TurnContext, metadata_for_test)。

tests::turn_context_sets_permission_profile_metadata383–416 ↗
fn turn_context_sets_permission_profile_metadata()

作用:这个测试确认:轮次上下文里的权限配置会被保存到元数据里。

数据流:它创建一份 workspace_write 权限配置,并把它放进 TurnContextItem。调用 apply_rollout_item 后,检查 metadata.sandbox_policy 等于这份权限配置序列化后的 JSON 字符串。

调用关系:它验证 apply_turn_context 使用 permission_profile 并转成字符串的行为,确保数据库里保存的是最终权限配置,而不是旧的或默认的沙箱字段。

调用图:调用 2 个内部函数(workspace_write, apply_rollout_item);外部调用 4 个(from, assert_eq!, TurnContext, metadata_for_test)。

tests::turn_context_sets_cwd_when_session_cwd_missing419–449 ↗
fn turn_context_sets_cwd_when_session_cwd_missing()

作用:这个测试确认:如果会话信息没有工作目录,轮次上下文可以作为备用来源补上目录。

数据流:它先把测试元数据的 cwd 清空,再应用一个带 /fallback/workspace 的 TurnContextItem。调用后检查 cwd 被设置为这个备用目录。

调用关系:它验证 apply_turn_context 的补缺逻辑:只在元数据原来没有 cwd 时才写入。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 6 个(from, new, new_read_only_policy, assert_eq!, TurnContext, metadata_for_test)。

tests::turn_context_sets_model_and_reasoning_effort452–482 ↗
fn turn_context_sets_model_and_reasoning_effort()

作用:这个测试确认:轮次上下文会记录使用的模型和推理强度。

数据流:它创建一个 TurnContextItem,模型是 gpt-5,推理强度是 High。调用 apply_rollout_item 后,检查 metadata.model 和 metadata.reasoning_effort 都被正确设置。

调用关系:它验证 apply_turn_context 对模型相关字段的写入,和另一个测试一起说明 SessionMeta 不负责这些字段。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 5 个(from, new_read_only_policy, assert_eq!, TurnContext, metadata_for_test)。

tests::session_meta_does_not_set_model_or_reasoning_effort485–518 ↗
fn session_meta_does_not_set_model_or_reasoning_effort()

作用:这个测试确认:会话信息不会设置具体模型和推理强度,因为这些属于轮次上下文。

数据流:它应用一条 SessionMetaLine,里面有会话来源、目录、版本和模型提供商等信息。调用后检查 metadata.model 和 metadata.reasoning_effort 仍然是 None。

调用关系:它验证 apply_session_meta_from_item 的边界,防止它越权写入应该由 apply_turn_context 维护的字段。

调用图:调用 1 个内部函数(apply_rollout_item);外部调用 4 个(from, assert_eq!, SessionMeta, metadata_for_test)。

tests::metadata_for_test520–549 ↗
fn metadata_for_test() -> ThreadMetadata

作用:这个测试辅助函数生成一份稳定的 ThreadMetadata,方便所有测试从同一个起点开始。

数据流:它创建固定线程 ID、固定创建时间和一批默认字段,比如 rollout 路径、来源、模型提供商、工作目录、token 数等。输出是一份可被测试修改的 ThreadMetadata。

调用关系:大多数测试都会先调用它,再把不同的 RolloutItem 应用上去。它让测试只关注要验证的那一两个字段,不用每次重复搭建完整元数据。

调用图:调用 1 个内部函数(from_string);外部调用 4 个(from_timestamp, from, new, from_u128)。

tests::diff_fields_detects_changes552–561 ↗
fn diff_fields_detects_changes()

作用:这个测试确认 ThreadMetadata 的字段差异检测能找出哪些字段变了。

数据流:它先造一份基础元数据,再复制一份并改掉 title 和 tokens_used。调用 diff_fields 后,检查返回的变更字段正好是 title 和 tokens_used。

调用关系:它不直接测试本文件的提取函数,而是验证 ThreadMetadata 自身的比较能力;这对判断哪些元数据需要写回数据库很有帮助。

调用图:调用 1 个内部函数(from_string);外部调用 3 个(now_v7, assert_eq!, metadata_for_test)。

thread-store/src/local/archive_thread.rs源码 ↗
domain_logicrequest handling

这个文件解决的是“用户不想再把某个线程当作活跃会话看见”的问题。它做的事很像把桌面上的文件放进档案柜:先根据线程编号找到对应的会话文件,再确认这个文件确实在允许的 sessions 目录里,避免误挪别的文件;然后创建 archived sessions 归档目录,把文件移动过去。它还会尽量更新状态数据库,状态数据库可以理解成一张索引表,记录每个线程的文件位置、是否归档、归档时间等信息。这里有个重要细节:移动文件失败会报错,因为这是归档的核心动作;但更新数据库失败不会让整个归档失败,因为文件已经移动成功,系统更看重不要把用户文件卡在半路。下面的测试分别确认文件确实被挪到了归档目录,以及有 SQLite 元数据时也会同步更新。

函数细节3
archive_thread11–61 ↗
async fn archive_thread(
    store: &LocalThreadStore,
    params: ArchiveThreadParams,
) -> ThreadStoreResult<()>

作用:把指定线程对应的本地会话文件移动到归档目录,并尽量更新状态数据库里的记录。有人点击“归档线程”或上层接口收到归档请求时,就会走到这里。

数据流:输入是本地线程仓库 store 和归档参数 params,里面最重要的是 thread_id。它先读取仓库配置和可选的状态数据库上下文,用线程编号找到原来的会话文件;接着检查路径是否安全、文件名是否匹配这个线程;然后创建归档文件夹,把原文件改名移动到归档位置;最后如果有状态数据库,就写入新的文件路径和当前归档时间。成功时没有返回数据,只表示事情办完;找不到线程、路径不对或移动失败时返回错误。

调用关系:它是本文件的核心动作,会被上层的归档入口调用。它自己把工作拆给几个帮手:state_db 取数据库上下文,find_thread_path_by_id_str 找会话文件,scoped_rollout_path 做路径安全检查,matching_rollout_file_name 确认文件名对应目标线程;真正落到磁盘上时交给 create_dir_all 建目录、rename 移动文件;最后用 now 取当前时间写入归档时间。

调用图:调用 3 个内部函数(state_db, matching_rollout_file_name, scoped_rollout_path);被 1 处调用(archive_thread);外部调用 4 个(now, find_thread_path_by_id_str, create_dir_all, rename)。

tests::archive_thread_moves_rollout_to_archived_collection82–125 ↗
async fn archive_thread_moves_rollout_to_archived_collection()

作用:这个测试确认最基本的归档行为:归档后,原来的活跃会话文件不见了,归档目录里出现同一个文件,并且列表查询能把它当成已归档线程看到。

数据流:它先创建一个临时 home 目录,做一个本地线程仓库,再写入一个假的会话文件。然后调用归档功能。调用之后,它检查原路径已经不存在,归档路径已经存在;再用 list_threads 按“只看归档”的方式查询,确认返回的线程编号、文件路径和归档时间都符合预期。

调用关系:它围绕 archive_thread 做端到端检查,也就是不只看函数返回成功,还检查磁盘文件和线程列表这两个外部可见结果。它会用 test_config 准备测试配置,用 write_session_file 造测试会话文件,用 from_string 和 from_u128 准备固定线程编号。

调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 5 个(new, from_u128, new, assert!, assert_eq!)。

tests::archive_thread_updates_sqlite_metadata_when_present128–177 ↗
async fn archive_thread_updates_sqlite_metadata_when_present()

作用:这个测试确认如果系统启用了 SQLite 状态数据库,归档线程时数据库里的线程位置和归档时间也会被更新。SQLite 是一种本地小型数据库,这里用来保存线程索引信息。

数据流:它先建临时目录和测试配置,写入一个假的活跃会话文件;再初始化状态数据库,把一条线程元数据写进去。随后调用归档功能。最后它从数据库重新读取这条线程记录,检查 rollout_path 已经变成归档目录下的新路径,并且 archived_at 归档时间已经有值。

调用关系:它验证 archive_thread 和状态数据库之间的配合。测试前半段用 StateRuntime 初始化数据库,用 ThreadMetadataBuilder 拼出一条线程记录并 upsert_thread 写入;测试核心仍然是调用归档;测试后半段用 get_thread 读回数据库,确认 archive_thread 在移动文件之后确实调用了数据库的归档标记能力。

调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 5 个(new, now, from_u128, assert!, assert_eq!)。

thread-store/src/local/unarchive_thread.rs源码 ↗
domain_logicrequest handling

可以把这里的逻辑想成把一本书从仓库书架搬回阅览室。unarchive_thread 先根据对话 ID 找到归档文件在哪里;如果找不到,就明确报错。找到后,它会确认这个文件真的在允许的归档目录里,避免有人用奇怪路径把系统带到别的地方。接着它从文件名里读出日期,把文件搬回普通 sessions/年/月/日 目录,并更新文件修改时间,让它像刚恢复过一样。若本地还有 SQLite 状态库(一个本地小数据库,用来快速查对话元信息),它也会把这条对话标记为“未归档”。最后,它重新读取这个会话文件,组装成 StoredThread 返回。下面的两个测试分别确认:文件确实被搬回来了、返回的信息正确;以及有 SQLite 元数据时,数据库里的路径和归档状态也会同步更新。

函数细节3
unarchive_thread15–101 ↗
async fn unarchive_thread(
    store: &LocalThreadStore,
    params: ArchiveThreadParams,
) -> ThreadStoreResult<StoredThread>

作用:把指定 ID 的归档对话恢复成普通对话。调用者给它一个对话 ID,它负责找到归档文件、搬回正确位置、更新状态,并返回恢复后的对话摘要信息。

数据流:输入是本地存储对象 store 和包含 thread_id 的参数。它先读取可能存在的状态数据库上下文,再按对话 ID 查归档文件;然后检查归档路径是否安全、文件名是否匹配这个对话、文件名里是否能拆出日期。确认无误后,它创建目标日期目录,把文件从归档目录移动到普通会话目录,刷新文件修改时间;如果有状态数据库,就把数据库记录改成未归档并更新路径。最后它重新读取恢复后的会话文件,把文件内容转成 StoredThread 输出;任何关键步骤失败都会变成清楚的 ThreadStoreError

调用关系:这是恢复归档对话的核心步骤,通常由本地线程存储的对外 unarchive_thread 接口触发。它自己不直接解析所有细节,而是把找归档文件交给 find_archived_thread_path_by_id_str,把路径安全检查交给 scoped_rollout_path,把文件名检查交给 matching_rollout_file_name,把日期拆分交给 rollout_date_parts,把最终会话文件读取交给 read_thread_item_from_rollout,再用 stored_thread_from_rollout_item 变成系统内部统一使用的对话对象。

调用图:调用 5 个内部函数(state_db, matching_rollout_file_name, scoped_rollout_path, stored_thread_from_rollout_item, touch_modified_time);被 1 处调用(unarchive_thread);外部调用 6 个(find_archived_thread_path_by_id_str, read_thread_item_from_rollout, rollout_date_parts, format!, create_dir_all, rename)。

tests::unarchive_thread_restores_rollout_and_returns_updated_thread119–146 ↗
async fn unarchive_thread_restores_rollout_and_returns_updated_thread()

作用:验证最基本的恢复归档流程真的可用。它确认归档文件会消失,普通会话目录会出现恢复后的文件,并且返回的对话信息显示为未归档。

数据流:测试先创建一个临时目录,搭一个本地存储对象,再写入一份假的归档会话文件。随后调用 store.unarchive_thread。调用之后,它检查旧归档路径不存在、新的 sessions/2025/01/03 路径存在,并检查返回的线程 ID、文件路径、归档时间、预览文字和第一条用户消息都符合预期。

调用关系:这个测试像一次完整彩排,直接走公开的本地存储恢复接口,而不是只测内部小函数。它用 test_config 准备测试配置,用 write_archived_session_file 造出归档文件,然后通过断言来证明 unarchive_thread 把文件移动和返回结果都做好了。

调用图:调用 4 个内部函数(from_string, new, test_config, write_archived_session_file);外部调用 4 个(new, from_u128, assert!, assert_eq!)。

tests::unarchive_thread_updates_sqlite_metadata_when_present149–199 ↗
async fn unarchive_thread_updates_sqlite_metadata_when_present()

作用:验证当系统启用了 SQLite 状态库时,恢复归档不只是移动文件,也会同步更新数据库里的元数据。这样列表页或搜索功能以后看到的状态才不会还是“已归档”。

数据流:测试先建临时目录和配置,写入一份归档会话文件,再初始化状态数据库运行时。它手工往数据库里插入一条已归档的对话元数据。接着调用 store.unarchive_thread。最后它从数据库重新读这条记录,确认数据库里的 rollout_path 已经变成恢复后的普通会话路径,archived_at 也变成空,表示不再归档。

调用关系:这个测试覆盖的是带数据库的分支。它先用 StateRuntime::init 建好本地状态库,再用元数据构建器造出一条已归档记录;当恢复接口运行后,它检查内部调用 mark_unarchived 的效果是否真的落到数据库里,防止文件系统和数据库状态不一致。

调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_archived_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。

thread-store/src/local/update_thread_metadata.rs源码 ↗
domain_logicrequest handling

一个线程的内容主要存在 rollout 文件里,可以理解成一本文字流水账;同时系统还有 SQLite 数据库,可以理解成一张方便检索的目录卡片。这个文件做的事,就是收到一小块“要改哪些字段”的补丁后,先尽量更新目录卡片,再在必要时往流水账里追加一条兼容旧系统的新记录。它会特别小心:空补丁就只读取线程;普通观察到的元数据写库失败通常只报警,不轻易让会话保存失败;但只改 Git 信息时,因为要保留没写明的旧字段,SQLite 读写就变得必须成功。它还会处理已归档线程、正在写入中的线程、外部路径线程,以及名字索引、记忆模式和 Git 信息之间互不覆盖的问题。

函数细节39
update_thread_metadata37–176 ↗
async fn update_thread_metadata(
    store: &LocalThreadStore,
    params: UpdateThreadMetadataParams,
) -> ThreadStoreResult<StoredThread>

作用:这是修改线程元数据的主入口。别人想改线程名字、Git 信息、记忆模式等,都会把线程编号和补丁交给它。

数据流:输入是本地线程仓库、线程编号、补丁内容和是否允许查归档线程;它先判断补丁是不是空,再更新 SQLite 里的元数据,必要时同步 rollout 日志文件、名字索引和 Git 信息;最后读回一个最新的 StoredThread 返回。如果中途发现线程不存在、路径不对或关键写入失败,就返回错误。

调用关系:它像总调度员一样串起整个流程:先问 needs_rollout_compatibility_update 和 sqlite_write_failure_should_block 要不要做额外兼容工作,再把主要写库工作交给 apply_metadata_update,之后按需要调用 resolve_rollout_path、apply_thread_memory_mode、apply_thread_name、apply_thread_git_info_to_rollout、apply_thread_git_info,最后用 read_thread 或 read_thread_by_rollout_path 读回结果。

调用图:调用 17 个内部函数(reconcile_rollout, state_db, git_info_from_parts, persist_thread, rollout_path, read_thread, read_thread_by_rollout_path, apply_metadata_update, apply_thread_git_info, apply_thread_git_info_to_rollout (+7 more));被 1 处调用(update_thread_metadata);外部调用 1 个(format!)。

refresh_resolved_rollout_path178–182 ↗
async fn refresh_resolved_rollout_path(resolved: &mut ResolvedRolloutPath)

作用:这个函数用来刷新已经找到的 rollout 文件路径。因为写入或归档后,实际文件路径可能被系统换成另一个现存路径。

数据流:输入是一个可修改的 ResolvedRolloutPath;它拿当前路径去查是否有更准确的现存 rollout 路径;如果查到了,就把里面的 path 替换掉,不返回额外结果。

调用关系:update_thread_metadata 在往 rollout 追加记忆模式或 Git 信息后会叫它检查路径,避免后续步骤还拿着旧路径继续写。

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

apply_metadata_update184–353 ↗
async fn apply_metadata_update(
    store: &LocalThreadStore,
    thread_id: ThreadId,
    patch: ThreadMetadataPatch,
    include_archived: bool,
    require_sqlite_write: bool,
) -> ThreadStoreResul

作用:这个函数负责把补丁里的大部分元数据写进 SQLite 状态库,并读回更新后的线程。它是主入口里最核心的“改目录卡片”步骤。

数据流:输入是仓库、线程编号、补丁、是否包含归档线程,以及 SQLite 写失败是否必须中断;它会找当前 rollout 路径,读取旧元数据,没有时按补丁和 rollout 文件创建一条新元数据,然后逐个字段覆盖更新;最后写回数据库,并读取线程返回。它可能修改 SQLite 中的线程行和记忆模式记录。

调用关系:update_thread_metadata 会先调用它完成基础更新。它内部会借助 resolve_rollout_path 找文件,调用 enum_to_string、normalize_cwd、memory_mode_as_str 做格式转换,还会用 resolve_git_info_patch 合并 Git 局部补丁;最后交给 read_thread 读回结果。

调用图:调用 11 个内部函数(state_db, git_info_from_parts, permission_profile_to_metadata_value, rollout_path, read_thread, enum_to_string, memory_mode_as_str, normalize_cwd, resolve_git_info_patch, resolve_rollout_path (+1 more));被 1 处调用(update_thread_metadata);外部调用 2 个(clone, warn!)。

needs_rollout_compatibility_update355–363 ↗
fn needs_rollout_compatibility_update(patch: &ThreadMetadataPatch) -> bool

作用:这个函数判断这次更新是否还需要写一份到 rollout 日志里,照顾老的读取方式。不是所有字段都需要这样做。

数据流:输入是一份元数据补丁;它检查是否改了名字、记忆模式或 Git 信息,以及补丁是否已经包含从会话中观察到的普通元数据;输出一个布尔值,表示要不要做 rollout 兼容更新。

调用关系:update_thread_metadata 在写完 SQLite 前后用它决定是否还要追加 rollout 记录、更新名字索引或同步 Git 信息。它会把“是不是观察型元数据”的判断交给 has_observed_metadata_facts。

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

sqlite_write_failure_should_block365–372 ↗
fn sqlite_write_failure_should_block(patch: &ThreadMetadataPatch) -> bool

作用:这个函数判断 SQLite 写失败时,这次请求是不是必须失败。它保护一个重要边界:可选索引坏了,不应该轻易影响原始会话日志的可靠保存。

数据流:输入是一份补丁;它判断是否是“只显式更新 Git 信息”这类必须依赖 SQLite 旧值的情况;输出 true 表示 SQLite 失败要拦住整个更新,false 表示可以只记警告。

调用关系:update_thread_metadata 调用它,把结果传给 apply_metadata_update。它也用 has_observed_metadata_facts 区分普通观察元数据和显式 Git-only 更新。

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

sqlite_write_error_is_best_effort374–376 ↗
fn sqlite_write_error_is_best_effort(err: &ThreadStoreError) -> bool

作用:这个函数判断某个 SQLite 写入错误能不能当作“尽力而为”的失败处理。也就是说,是否只报警、不一定让用户操作失败。

数据流:输入是 ThreadStoreError;它检查错误类型,目前内部错误被视为可降级;输出布尔值。

调用关系:apply_metadata_update 在 SQLite 更新失败后调用它,再结合 sqlite_write_failure_should_block 的结果决定是返回错误还是只打 warn 日志。

调用图:被 1 处调用(apply_metadata_update);外部调用 1 个(matches!)。

has_observed_metadata_facts378–397 ↗
fn has_observed_metadata_facts(patch: &ThreadMetadataPatch) -> bool

作用:这个函数检查补丁里有没有“从真实会话观察出来”的元数据,比如预览、模型、时间、工作目录、token 用量等。它用来区分普通同步信息和用户明确发起的特殊修改。

数据流:输入是一份补丁;它逐项查看许多字段是否出现;只要有一个观察型字段存在,就输出 true,否则输出 false。

调用关系:needs_rollout_compatibility_update 和 sqlite_write_failure_should_block 都依赖它做分流:有观察型事实时,通常不需要额外 rollout 兼容写入,也不会让 SQLite 可选失败过度影响流程。

调用图:被 2 处调用(needs_rollout_compatibility_update, sqlite_write_failure_should_block)。

enum_to_string399–405 ↗
fn enum_to_string(value: &T) -> String

作用:这个小工具把枚举值转成字符串。枚举可以理解成一组选项,比如来源类型、审批模式等。

数据流:输入是一个可序列化的值;它先用 JSON 方式转成通用值,如果本来就是字符串就取字符串,否则取 JSON 文本,失败时给空字符串;输出字符串。

调用关系:apply_metadata_update 用它把 source、approval_mode 这类选项保存到 SQLite 元数据里,避免数据库里存复杂对象。

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

normalize_cwd407–409 ↗
fn normalize_cwd(cwd: PathBuf) -> PathBuf

作用:这个函数把工作目录路径整理成更标准的样子。比如把路径里的“child/..”这类绕路写法折叠掉。

数据流:输入是一个 PathBuf 路径;它尝试用路径规范化工具清理路径,成功就返回清理后的路径,失败就保留原路径;不改磁盘文件。

调用关系:apply_metadata_update 在保存 cwd,也就是当前工作目录时调用它。这样后面按目录筛选线程时,不会因为同一个目录有不同写法而匹配不上。

调用图:被 1 处调用(apply_metadata_update);外部调用 2 个(as_path, normalize_for_path_comparison)。

apply_thread_git_info411–441 ↗
async fn apply_thread_git_info(
    store: &LocalThreadStore,
    thread_id: ThreadId,
    sha: &Option<String>,
    branch: &Option<String>,
    origin_url: &Option<String>,
) -> ThreadStoreResult<()

作用:这个函数把最终确定的 Git 信息写进 SQLite。Git 信息包括提交号、分支名和仓库地址。

数据流:输入是仓库、线程编号,以及可能为空的 sha、branch、origin_url;它拿到状态库后执行更新;输出成功或错误。如果数据库不可用,或线程元数据在更新前消失,就返回内部错误。

调用关系:update_thread_metadata 在已经把 Git 信息写进 rollout 后调用它,确保 SQLite 目录卡片也和日志一致。

调用图:调用 1 个内部函数(state_db);被 1 处调用(update_thread_metadata);外部调用 1 个(format!)。

resolve_git_info_patch443–459 ↗
fn resolve_git_info_patch(
    existing: Option<GitInfo>,
    git_info: GitInfoPatch,
) -> (Option<String>, Option<String>, Option<String>)

作用:这个函数把“只改部分 Git 字段”的补丁补全成完整结果。比如只改分支时,提交号和仓库地址要沿用旧值。

数据流:输入是旧 Git 信息和新的 GitInfoPatch;它分别处理提交号、分支、仓库地址三个字段:补丁里明确给了就用补丁,没给就用旧值;输出三个最终字段。

调用关系:apply_metadata_update 写 SQLite 时会用它合并 Git 补丁;update_thread_metadata 在兼容写 rollout 前也会用它,保证追加到日志里的 Git 信息不会意外丢字段。

调用图:被 2 处调用(apply_metadata_update, update_thread_metadata)。

apply_thread_git_info_to_rollout461–495 ↗
async fn apply_thread_git_info_to_rollout(
    rollout_path: &Path,
    thread_id: ThreadId,
    sha: &Option<String>,
    branch: &Option<String>,
    origin_url: &Option<String>,
    memory_mode: Op

作用:这个函数把 Git 信息追加写到 rollout 会话日志里。这样即使以后从原始日志重建状态,也能看到最新 Git 信息。

数据流:输入是 rollout 文件路径、线程编号、Git 三个字段和可选记忆模式;它先读取日志开头的会话元信息,确认里面的线程编号和要改的一致;然后填入 Git 信息和记忆模式,再把一条新的 SessionMeta 记录追加到文件末尾;输出成功或错误。

调用关系:update_thread_metadata 在需要兼容 rollout 的 Git 更新时调用它。它依赖 read_session_meta_line 读旧元信息,依赖 append_rollout_item_to_path 追加新记录。

调用图:被 1 处调用(update_thread_metadata);外部调用 4 个(append_rollout_item_to_path, read_session_meta_line, format!, SessionMeta)。

apply_thread_name497–521 ↗
async fn apply_thread_name(
    store: &LocalThreadStore,
    thread_id: ThreadId,
    name: String,
) -> ThreadStoreResult<()>

作用:这个函数更新线程名字,并把名字放进专门的名字索引里,方便以后快速查到。名字就像给一段聊天贴上的标题标签。

数据流:输入是仓库、线程编号和新名字;它如果有 SQLite,就先更新数据库里的 title;然后在 codex_home 下追加名字索引记录;输出成功或错误。

调用关系:update_thread_metadata 在补丁里包含 name 时调用它。它一边维护 SQLite,一边调用 append_thread_name 维护旧的 rollout 名字索引。

调用图:调用 1 个内部函数(state_db);被 1 处调用(update_thread_metadata);外部调用 2 个(append_thread_name, format!)。

apply_thread_memory_mode523–552 ↗
async fn apply_thread_memory_mode(
    rollout_path: &Path,
    thread_id: ThreadId,
    memory_mode: ThreadMemoryMode,
) -> ThreadStoreResult<()>

作用:这个函数把线程的记忆模式写进 rollout 日志。记忆模式表示这个线程是否启用长期记忆。

数据流:输入是 rollout 路径、线程编号和 Enabled/Disabled 这样的模式;它读取会话元信息并核对线程编号,然后清空本条记录里的 Git 字段以避免误覆盖,只写 memory_mode,最后追加一条 SessionMeta 到日志;输出成功或错误。

调用关系:update_thread_metadata 在补丁包含 memory_mode 且需要 rollout 兼容时调用它。它内部用 memory_mode_as_str 把模式转成文本,并用 append_rollout_item_to_path 写文件。

调用图:调用 1 个内部函数(memory_mode_as_str);被 1 处调用(update_thread_metadata);外部调用 4 个(append_rollout_item_to_path, read_session_meta_line, format!, SessionMeta)。

memory_mode_as_str554–559 ↗
fn memory_mode_as_str(mode: ThreadMemoryMode) -> &'static str

作用:这个函数把记忆模式选项转成固定字符串。数据库和日志里需要的是文字,而不是程序里的枚举值。

数据流:输入是 ThreadMemoryMode;Enabled 变成 "enabled",Disabled 变成 "disabled";输出静态字符串。

调用关系:apply_metadata_update 写 SQLite 的记忆模式时会用它;apply_thread_memory_mode 写 rollout 日志时也会用它,保证两边用同一套文字。

调用图:被 2 处调用(apply_metadata_update, apply_thread_memory_mode)。

resolve_rollout_path561–608 ↗
async fn resolve_rollout_path(
    store: &LocalThreadStore,
    thread_id: ThreadId,
    include_archived: bool,
) -> ThreadStoreResult<ResolvedRolloutPath>

作用:这个函数负责找到某个线程对应的 rollout 文件在哪里。没有这个文件路径,就没法从原始会话日志验证或追加元数据。

数据流:输入是仓库、线程编号和是否允许查归档;它先看 live_writer 里有没有正在使用的路径,再查普通会话文件,必要时查归档会话文件;输出路径和它是否归档。找不到或查找失败就返回请求错误。

调用关系:update_thread_metadata 和 apply_metadata_update 都会在需要真实文件位置时调用它。它会使用 rollout_path_is_archived 判断路径是否在归档目录中,并借助 find_thread_path_by_id_str、find_archived_thread_path_by_id_str 搜索文件。

调用图:调用 3 个内部函数(state_db, rollout_path, rollout_path_is_archived);被 2 处调用(apply_metadata_update, update_thread_metadata);外部调用 4 个(find_archived_thread_path_by_id_str, find_thread_path_by_id_str, format!, to_string)。

rollout_path_is_archived610–612 ↗
fn rollout_path_is_archived(store: &LocalThreadStore, path: &Path) -> bool

作用:这个函数判断一个 rollout 路径是不是在归档目录下面。归档线程要保持归档状态,不能因为更新元数据又被当成普通线程。

数据流:输入是仓库和一个路径;它检查路径是否以 codex_home 下的归档会话目录开头;输出 true 或 false。

调用关系:resolve_rollout_path 用它给找到的路径打上 archived 标记;apply_metadata_update 也间接依赖这个标记决定新建 SQLite 元数据时是否填 archived_at。

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

tests::update_thread_metadata_sets_name_on_active_rollout_and_indexes_name638–662 ↗
async fn update_thread_metadata_sets_name_on_active_rollout_and_indexes_name()

作用:这个测试确认给普通活动线程改名后,读回线程能看到新名字,名字索引也能查到新名字。

数据流:它创建临时目录和一份会话文件,调用 update_thread_metadata 设置 name,然后读取返回线程和名字索引;期望两边都变成指定名字。

调用关系:它覆盖 update_thread_metadata、apply_thread_name 以及 append_thread_name 这条路径,证明改名不只写数据库,也兼容旧索引。

调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 5 个(default, new, from_u128, assert_eq!, find_thread_name_by_id)。

tests::update_thread_metadata_sets_memory_mode_on_active_rollout665–702 ↗
async fn update_thread_metadata_sets_memory_mode_on_active_rollout()

作用:这个测试确认给活动线程设置记忆模式时,rollout 日志和 SQLite 都会记录下来。

数据流:它准备临时会话文件和状态库,调用更新接口把 memory_mode 设为 disabled,然后检查返回线程、最后一条 rollout 记录和数据库里的记忆模式。

调用关系:它主要验证 update_thread_metadata 调用 apply_metadata_update 和 apply_thread_memory_mode 后,两种存储都保持一致。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 5 个(default, new, from_u128, assert_eq!, last_rollout_item)。

tests::update_thread_metadata_preserves_memory_mode_when_updating_git_info705–771 ↗
async fn update_thread_metadata_preserves_memory_mode_when_updating_git_info()

作用:这个测试确认更新 Git 信息时,不会把之前设置好的记忆模式弄丢。

数据流:它先把记忆模式设为 disabled,再只更新 Git 分支;随后检查 rollout 最后一条记录同时包含 disabled 和新分支,并重新 reconcile 后确认数据库仍保留记忆模式。

调用关系:它验证 update_thread_metadata 在 Git 更新前读取旧 memory_mode,并传给 apply_thread_git_info_to_rollout,避免 Git 更新覆盖记忆模式。

调用图:调用 6 个内部函数(from_string, reconcile_rollout, init, new, test_config, write_session_file);外部调用 5 个(default, new, from_u128, assert_eq!, last_rollout_item)。

tests::update_thread_metadata_uses_live_rollout_path_for_external_resume774–811 ↗
async fn update_thread_metadata_uses_live_rollout_path_for_external_resume()

作用:这个测试确认线程如果是从外部 rollout 文件恢复的,更新元数据时会写到那个外部文件,而不是误写到默认目录。

数据流:它在外部临时目录创建会话文件,先 resume_thread 让仓库记住这个 live 路径,再设置记忆模式;最后检查外部文件末尾确实追加了记录。

调用关系:它覆盖 resolve_rollout_path 优先使用 live_writer::rollout_path 的行为,保证外部恢复线程也能被正确更新。

调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 7 个(default, new, from_u128, assert!, assert_eq!, last_rollout_item, test_thread_metadata)。

tests::update_thread_metadata_sets_git_info814–854 ↗
async fn update_thread_metadata_sets_git_info()

作用:这个测试确认可以一次性写入完整 Git 信息:提交号、分支和仓库地址。

数据流:它创建线程和状态库,传入包含三个 Git 字段的补丁;返回线程后检查 git_info 里的三个值都正确。

调用关系:它验证 update_thread_metadata、resolve_git_info_patch、apply_thread_git_info_to_rollout 和 apply_thread_git_info 组成的 Git 写入链路。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 4 个(default, new, from_u128, assert_eq!)。

tests::update_thread_metadata_sets_permission_profile857–894 ↗
async fn update_thread_metadata_sets_permission_profile()

作用:这个测试确认权限配置会被正确保存。权限配置可以理解成系统允许这段会话做哪些操作的安全档位。

数据流:它创建线程和状态库,更新 permission_profile 为 Disabled,然后检查返回线程和 SQLite 里的 sandbox_policy 都反映这个设置。

调用关系:它覆盖 apply_metadata_update 对 permission_profile_to_metadata_value 的使用,保证程序类型和数据库文本之间转换正确。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 4 个(default, new, from_u128, assert_eq!)。

tests::update_thread_metadata_partially_updates_git_info897–952 ↗
async fn update_thread_metadata_partially_updates_git_info()

作用:这个测试确认 Git 信息可以只改一个字段,并保留其他旧字段。

数据流:它先写入完整 Git 信息,再只把分支改成 feature;读回后期望提交号和仓库地址仍是旧值,只有分支变了。

调用关系:它直接验证 resolve_git_info_patch 的核心意义:局部补丁不是整块替换,不会误删没提到的 Git 字段。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 4 个(default, new, from_u128, assert_eq!)。

tests::update_thread_metadata_clears_git_info_fields955–1123 ↗
async fn update_thread_metadata_clears_git_info_fields()

作用:这个测试确认 Git 字段可以被明确清空,并且后续设置记忆模式不会把已清空的 Git 信息“复活”。

数据流:它先写完整 Git 信息,再用 Some(None) 清空三个字段,检查返回线程没有 Git 信息、rollout 里 git 是空对象;之后多次 reconcile、更新记忆模式、删除 SQLite 行并局部更新,确认清空和局部恢复的行为都符合预期。

调用关系:它覆盖 resolve_git_info_patch、apply_thread_git_info_to_rollout、apply_thread_memory_mode 和 reconcile_rollout 的组合边界,防止 Git 信息在不同存储之间来回同步时出错。

调用图:调用 6 个内部函数(from_string, reconcile_rollout, init, new, test_config, write_session_file);外部调用 6 个(default, new, from_u128, assert!, assert_eq!, last_rollout_item)。

tests::update_thread_metadata_rejects_mismatched_session_meta_id1126–1155 ↗
async fn update_thread_metadata_rejects_mismatched_session_meta_id()

作用:这个测试确认如果 rollout 文件里的会话编号和要更新的线程编号对不上,系统会拒绝写入。

数据流:它创建会话文件后故意把文件内容里的编号改成另一个编号,再尝试设置记忆模式;期望得到内部错误,并且错误信息包含 metadata id mismatch。

调用关系:它验证 apply_thread_memory_mode 里的安全检查,避免把一个线程的元数据误写进另一个线程的日志。

调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 6 个(default, new, from_u128, assert!, read_to_string, write)。

tests::update_thread_metadata_applies_combined_explicit_patch1158–1208 ↗
async fn update_thread_metadata_applies_combined_explicit_patch()

作用:这个测试确认一次补丁里同时改名字、记忆模式和 Git 信息时,三件事都会正确完成。

数据流:它创建线程和状态库,提交一个包含 name、memory_mode、git_info 的补丁;随后检查返回线程、rollout 末尾记录、名字索引和数据库记忆模式。

调用关系:它覆盖 update_thread_metadata 的综合调度能力,证明 apply_thread_name、apply_thread_memory_mode、apply_thread_git_info_to_rollout 等步骤可以一起协作。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 6 个(default, new, from_u128, assert_eq!, find_thread_name_by_id, last_rollout_item)。

tests::sqlite_failures_are_best_effort_for_legacy_rollout_compat_updates1211–1220 ↗
fn sqlite_failures_are_best_effort_for_legacy_rollout_compat_updates()

作用:这个测试确认老式兼容更新里的 SQLite 失败不会默认阻断流程,比如改名字或记忆模式。

数据流:它构造只包含 name 或 memory_mode 的补丁,调用 sqlite_write_failure_should_block;期望结果都是 false。

调用关系:它验证 sqlite_write_failure_should_block 的容错策略,确保可选状态库坏掉时,不会让兼容 rollout 的更新显得像原始日志保存失败。

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

tests::sqlite_failures_are_best_effort_for_observed_metadata_updates1223–1237 ↗
fn sqlite_failures_are_best_effort_for_observed_metadata_updates()

作用:这个测试确认从会话里观察到的普通元数据更新,即使带有 Git 或记忆模式,也不要求 SQLite 失败时阻断。

数据流:它构造带 updated_at 的补丁,以及带 preview、git_info、memory_mode 的补丁;调用 sqlite_write_failure_should_block,期望都是 false。

调用关系:它验证 has_observed_metadata_facts 对 sqlite_write_failure_should_block 的影响,保证观察型同步保持“尽力而为”。

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

tests::sqlite_failures_still_block_for_explicit_git_only_updates1240–1248 ↗
fn sqlite_failures_still_block_for_explicit_git_only_updates()

作用:这个测试确认只显式更新 Git 信息时,SQLite 写失败仍然必须阻断。

数据流:它构造只包含 git_info 的补丁并调用 sqlite_write_failure_should_block;期望返回 true。

调用关系:它保护 Git 局部更新的正确性,因为只改 Git 某个字段时必须读旧值,SQLite 不可靠就不能随便继续。

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

tests::metadata_patch_applies_title_over_existing_name1251–1291 ↗
async fn metadata_patch_applies_title_over_existing_name()

作用:这个测试确认后来的 title 补丁会覆盖之前用户设置的 name。这里 title 是从会话内容推导出的标题。

数据流:它先给线程设置一个用户名字,再提交包含 title 和 preview 的补丁;最后读回线程,期望名字显示为新的 title。

调用关系:它验证 apply_metadata_update 中 name 和 title 的覆盖顺序:如果补丁里有 title,最终 title 字段以后者为准。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 4 个(default, new, from_u128, assert_eq!)。

tests::metadata_patch_applies_latest_preview_and_first_user_message1294–1349 ↗
async fn metadata_patch_applies_latest_preview_and_first_user_message()

作用:这个测试确认数据库会保存最新的预览和第一条用户消息,同时读线程时仍可能以 rollout 内容为准显示。

数据流:它先写一版 preview 和 first_user_message,再写另一版;随后检查返回线程的展示值来自会话文件里的实际用户消息,同时检查 SQLite 元数据保存的是后写入的值。

调用关系:它揭示 read_thread 和 SQLite 元数据之间的分工:apply_metadata_update 会更新目录卡片,但读出的展示字段可能由原始 rollout 内容重新计算。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 4 个(default, new, from_u128, assert_eq!)。

tests::observed_metadata_rejects_unknown_thread_without_rollout1352–1387 ↗
async fn observed_metadata_rejects_unknown_thread_without_rollout()

作用:这个测试确认不能凭空给不存在的线程创建观察型元数据。必须先有真实 rollout 文件证明这个线程存在。

数据流:它只创建状态库,不创建会话文件,然后尝试更新 preview;期望返回 thread not found,并确认 SQLite 里没有新元数据。

调用关系:它验证 apply_metadata_update 在缺少旧记录和 rollout 路径时会调用 resolve_rollout_path,并在找不到线程时拒绝创建幽灵记录。

调用图:调用 4 个内部函数(from_string, init, new, test_config);外部调用 4 个(default, new, from_u128, assert!)。

tests::update_thread_metadata_recreates_missing_archived_sqlite_row_as_archived1390–1427 ↗
async fn update_thread_metadata_recreates_missing_archived_sqlite_row_as_archived()

作用:这个测试确认归档线程即使 SQLite 行丢了,重新创建元数据时也会保留归档状态。

数据流:它只创建归档会话文件和状态库,然后更新 preview;读回线程和数据库后,都应看到 archived_at 有值。

调用关系:它验证 resolve_rollout_path 返回的 archived 标记会被 apply_metadata_update 用来设置新元数据的 archived_at。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_archived_session_file);外部调用 4 个(default, new, from_u128, assert!)。

tests::observed_metadata_normalizes_cwd_for_list_filters1430–1492 ↗
async fn observed_metadata_normalizes_cwd_for_list_filters()

作用:这个测试确认保存工作目录时会规范化路径,之后按目录筛选线程才能匹配到。

数据流:它创建 workspace/child/.. 这种不规整路径,更新为 cwd;然后检查 SQLite 里保存的是规范化后的 workspace,并用 list_threads 的 cwd_filters 成功筛到该线程。

调用关系:它验证 normalize_cwd 在 apply_metadata_update 中的作用,也证明规范化结果会被列表查询使用。

调用图:调用 5 个内部函数(from_string, init, new, test_config, write_session_file);外部调用 8 个(default, new, from_u128, new, assert_eq!, normalize_for_path_comparison, create_dir_all, vec!)。

tests::update_thread_metadata_keeps_archived_thread_archived_in_sqlite1495–1555 ↗
async fn update_thread_metadata_keeps_archived_thread_archived_in_sqlite()

作用:这个测试确认已经归档的线程改名后,在 SQLite 里仍然是归档状态。

数据流:它创建归档会话文件,先 reconcile 到数据库并确认 archived_at 存在,再更新线程名字;最后检查返回线程和数据库里的 archived_at 仍然存在。

调用关系:它覆盖 update_thread_metadata、apply_metadata_update 和 apply_thread_name 对归档线程的处理,防止改名把归档标记清掉。

调用图:调用 6 个内部函数(from_string, reconcile_rollout, init, new, test_config, write_archived_session_file);外部调用 4 个(default, new, from_u128, assert!)。

tests::update_thread_metadata_keeps_live_archived_thread_archived_in_sqlite1558–1619 ↗
async fn update_thread_metadata_keeps_live_archived_thread_archived_in_sqlite()

作用:这个测试确认归档线程即使被恢复成 live 状态来操作,改名后也不会丢掉归档标记。

数据流:它创建归档会话文件并写入数据库,再 resume_thread 让它成为当前 live 线程,然后更新名字;最后检查返回线程和 SQLite 都仍然有 archived_at。

调用关系:它验证 resolve_rollout_path 优先用 live 路径时,rollout_path_is_archived 仍能识别归档目录,从而让 apply_metadata_update 保持归档状态。

调用图:调用 6 个内部函数(from_string, reconcile_rollout, init, new, test_config, write_archived_session_file);外部调用 5 个(default, new, from_u128, assert!, test_thread_metadata)。

tests::test_thread_metadata1621–1627 ↗
fn test_thread_metadata() -> ThreadPersistenceMetadata

作用:这个测试辅助函数生成一份恢复线程时用的基础持久化元数据。它不是业务功能,只是给测试准备输入。

数据流:它读取当前工作目录,填入测试用 model_provider 和 enabled 记忆模式;输出 ThreadPersistenceMetadata。

调用关系:外部路径恢复和 live 归档恢复相关测试会调用它,作为 resume_thread 的 metadata 参数。

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

tests::last_rollout_item1629–1637 ↗
fn last_rollout_item(path: &std::path::Path) -> Value

作用:这个测试辅助函数读取 rollout 文件最后追加的那一条 JSON 记录,方便测试检查写入结果。

数据流:输入是 rollout 文件路径;它读完整个文件,取最后一行,按 JSON 解析成 Value;输出这条 JSON 记录。

调用关系:多个测试在调用 update_thread_metadata 后用它检查 apply_thread_memory_mode 或 apply_thread_git_info_to_rollout 是否真的往文件末尾写了期望内容。

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

核心结果发出

这些文件将流式模型和工具结果转换为持久化的轮次项目、生命周期回调、状态更新,以及用户可见的命令或 diff 记录。

core/src/tools/events.rs源码 ↗
orchestrationrequest handling / tool execution

可以把这个文件理解成工具执行过程里的“播报员”。工具一开始运行,它发出开始事件;工具结束后,它把标准输出、错误输出、退出码、耗时、状态这些信息整理好,再发出结束事件。对于改文件的补丁,它还会记录文件变化,并在需要时发出本轮代码差异。这里的“差异”就是类似代码审查里看到的 unified diff,用来告诉别人哪些行被加了或删了。文件里最核心的是 ToolEmitter,它按工具类型分成 shell 命令、应用补丁、统一执行命令三类,然后用同一套 begin/finish 流程对外播报。它还特别处理超时、沙箱拒绝、用户拒绝等失败情况,保证即使事情没成功,界面和模型也能收到清楚、一致的结果。

函数细节19
ToolEventCtx::new39–51 ↗
fn new(
        session: &'a Session,
        turn: &'a TurnContext,
        call_id: &'a str,
        turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,
    ) -> Self

作用:创建一个工具事件上下文,也就是把“这次事件属于哪个会话、哪一轮对话、哪个工具调用、是否要跟踪文件差异”打包在一起。后面发任何工具事件时都靠它知道该发给谁。

数据流:输入是会话对象、当前轮次对象、工具调用编号,以及可选的文件差异跟踪器 → 函数只是把这些引用装进 ToolEventCtx 结构里 → 输出一个上下文对象,不会发送事件,也不会改动外部状态。

调用关系:它通常在真正执行工具前由上层流程创建,比如普通命令执行、拦截 apply_patch、统一执行接口,以及这些文件里的测试。之后 ToolEmitter::begin、ToolEmitter::finish、emit_patch_end、emit_exec_end 等函数都会带着这个上下文继续工作。

调用图:被 9 处调用(assert_failed_apply_patch_tracks_committed_delta, invalidation_emits_empty_turn_diff, net_zero_patch_emits_empty_turn_diff, handle_call, intercept_apply_patch, run_exec_like, emit_exec_end_for_unified_exec, emit_failed_exec_end_for_unified_exec, exec_command)。

tracker_update_for_known_delta81–93 ↗
fn tracker_update_for_known_delta(
    environment_id: Option<&str>,
    delta: &'a AppliedPatchDelta,
) -> TurnDiffTrackerUpdate<'a>

作用:判断一次已知的补丁变化要不要记录到“本轮文件差异”里。它避免把“精确但什么都没改”的空变化当成真正改动。

数据流:输入是可选的环境编号和一份补丁差异 → 它先看这份差异是不是精确结果、又是不是空的 → 如果确实什么都没改,就返回不更新;否则返回“请把这份差异记下来”,并带上环境编号。

调用关系:它被 ToolEmitter::emit 在 apply_patch 成功或被拒绝但已有部分改动时使用。它把判断结果交给 emit_patch_end,后者再真正更新差异跟踪器并决定是否向外发 TurnDiff 事件。

调用图:调用 2 个内部函数(is_empty, is_exact)。

emit_exec_command_begin95–120 ↗
async fn emit_exec_command_begin(
    ctx: ToolEventCtx<'_>,
    command: &[String],
    cwd: &AbsolutePathBuf,
    parsed_cmd: &[ParsedCommand],
    source: ExecCommandSource,
    interaction_input:

作用:发送“命令开始执行了”的事件。用户界面或外部监听方可以靠这个事件立刻知道某条命令已经启动,而不是等到结束才看到结果。

数据流:输入是事件上下文、命令参数、当前目录、解析后的命令信息、命令来源、可选交互输入和进程编号 → 它补上当前时间戳,把这些内容组装成 ExecCommandBeginEvent → 通过 session 发送出去,不返回业务结果。

调用关系:它是命令类工具开始阶段的实际发报函数。emit_exec_stage 遇到 Begin 阶段时会调用它;ToolEmitter::emit 又负责把 shell 或 unified exec 的开始阶段转到 emit_exec_stage。

调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(emit_exec_stage);外部调用 3 个(to_vec, ExecCommandBegin, clone)。

ToolEmitter::shell144–152 ↗
fn shell(command: Vec<String>, cwd: AbsolutePathBuf, source: ExecCommandSource) -> Self

作用:创建一个用于普通 shell 命令的事件播报器。shell 命令就是像终端里输入的命令,例如 ls、git status 这类。

数据流:输入是命令参数、工作目录和命令来源 → 它用 parse_command 把原始命令拆成更容易展示和理解的结构 → 输出一个 ToolEmitter::Shell,里面保存原命令、目录、来源和解析结果。

调用关系:普通执行流程 run_exec_like 会用它来准备播报器。之后同一个播报器可以先 begin 发开始事件,再 finish 发结束事件。

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

ToolEmitter::apply_patch_for_environment154–164 ↗
fn apply_patch_for_environment(
        changes: HashMap<PathBuf, FileChange>,
        auto_approved: bool,
        environment_id: String,
    ) -> Self

作用:创建一个用于“应用补丁改文件”的事件播报器,并把这次改动归到某个环境编号下。这样系统能区分不同执行环境里的文件变化。

数据流:输入是文件变化清单、是否自动批准、环境编号 → 它把这些信息装成 ToolEmitter::ApplyPatch → 输出一个补丁播报器,不会立刻发送事件。

调用关系:handle_call 和 intercept_apply_patch 在准备执行或拦截补丁时会调用它。真正发开始、完成、失败、拒绝事件的工作,后面交给 ToolEmitter::emit、begin 和 finish。

调用图:被 2 处调用(handle_call, intercept_apply_patch)。

ToolEmitter::unified_exec166–180 ↗
fn unified_exec(
        command: &[String],
        cwd: AbsolutePathBuf,
        source: ExecCommandSource,
        process_id: Option<String>,
    ) -> Self

作用:创建一个用于“统一执行命令”接口的事件播报器。它和普通 shell 类似,但可以额外带进程编号,方便追踪同一个进程的开始和结束。

数据流:输入是命令、工作目录、来源和可选进程编号 → 它解析命令并复制必要信息 → 输出 ToolEmitter::UnifiedExec,供后续发开始或结束事件。

调用关系:exec_command 以及统一执行的成功/失败收尾流程会用它。它后面通过 ToolEmitter::emit 走到 emit_exec_stage,再发具体的 begin 或 end 事件。

调用图:调用 1 个内部函数(parse_command);被 3 处调用(emit_exec_end_for_unified_exec, emit_failed_exec_end_for_unified_exec, exec_command)。

ToolEmitter::emit182–338 ↗
async fn emit(&self, ctx: ToolEventCtx<'_>, stage: ToolEventStage<'_>)

作用:这是这个文件的核心分发器:同样是“开始、成功、失败”这些阶段,它会根据工具类型选择正确的事件格式发出去。简单说,它决定该播报“命令事件”还是“文件改动事件”。

数据流:输入是某个 ToolEmitter、事件上下文和当前阶段 → 它按工具类型和阶段分支处理:命令类转成执行开始/结束事件,补丁类转成文件变更开始/完成事件,并计算补丁状态和差异跟踪方式 → 输出主要是发送事件,可能还会触发文件差异更新。

调用关系:ToolEmitter::begin 和 ToolEmitter::finish 都调用它。它自己不直接执行命令或补丁,只负责把执行结果翻译成对外事件;具体发命令事件交给 emit_exec_stage,补丁结束交给 emit_patch_end。

调用图:调用 3 个内部函数(new, emit_exec_stage, emit_patch_end);被 2 处调用(begin, finish);外部调用 2 个(new, FileChange)。

ToolEmitter::begin340–342 ↗
async fn begin(&self, ctx: ToolEventCtx<'_>)

作用:发送某个工具“刚开始”的事件。调用方不用关心这是命令还是补丁,只要告诉播报器开始了即可。

数据流:输入是事件上下文 → 它把阶段固定成 ToolEventStage::Begin,并交给 ToolEmitter::emit → 输出是异步发送对应的开始事件。

调用关系:它是 begin 阶段的便捷入口。上层执行工具前调用它;它再把实际工作交给 ToolEmitter::emit。

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

ToolEmitter::format_exec_output_for_model344–350 ↗
fn format_exec_output_for_model(
        &self,
        output: &ExecToolCallOutput,
        ctx: ToolEventCtx<'_>,
    ) -> String

作用:把命令执行输出整理成适合发给模型看的文字。这里会考虑截断策略,避免输出太长把后续对话撑爆。

数据流:输入是命令输出和事件上下文 → 它读取当前轮次里的截断规则 → 调用外层的格式化函数生成一段文本 → 输出这段给模型看的结果字符串。

调用关系:ToolEmitter::finish 在命令成功、失败、超时、被沙箱拒绝时会调用它。它只负责“怎么写给模型看”,不负责发事件。

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

ToolEmitter::finish352–430 ↗
async fn finish(
        &self,
        ctx: ToolEventCtx<'_>,
        out: Result<ExecToolCallOutput, ToolError>,
        applied_patch_delta: Option<&AppliedPatchDelta>,
    ) -> Result<String, Func

作用:收尾一个工具调用:把执行结果变成事件发出去,同时决定要返回给模型的是成功文本还是错误文本。它是工具执行完以后最重要的出口。

数据流:输入是事件上下文、工具执行结果,以及可选的补丁差异 → 如果成功,它格式化输出并按退出码判断成功或需要告诉模型失败;如果超时、沙箱拒绝、普通错误或用户拒绝,它生成统一的失败说明和对应事件 → 最后调用 emit 发事件,并返回给调用方一个成功字符串或 FunctionCallError。

调用关系:上层工具运行完后会调用它。它负责把 ToolError 这类内部错误翻译成 ToolEventStage,再交给 ToolEmitter::emit;需要给模型看的文字则通过 ToolEmitter::format_exec_output_for_model 生成。

调用图:调用 2 个内部函数(emit, format_exec_output_for_model);外部调用 5 个(Message, Output, Failure, format!, RespondToModel)。

ExecCommandInput::new443–459 ↗
fn new(
        command: &'a [String],
        cwd: &'a AbsolutePathBuf,
        parsed_cmd: &'a [ParsedCommand],
        source: ExecCommandSource,
        interaction_input: Option<&'a str>,

作用:把一次命令事件需要的输入信息打包起来,避免后面函数传一长串参数。它相当于给“命令播报”准备一张信息表。

数据流:输入是命令、当前目录、解析后的命令、来源、可选交互输入和可选进程编号 → 它把这些引用和小字段放进 ExecCommandInput → 输出这个打包后的对象,不修改任何外部状态。

调用关系:ToolEmitter::emit 在处理 Shell 和 UnifiedExec 时会创建它。随后 emit_exec_stage 和 emit_exec_end 用它来生成开始或结束事件。

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

emit_exec_stage472–534 ↗
async fn emit_exec_stage(
    ctx: ToolEventCtx<'_>,
    exec_input: ExecCommandInput<'_>,
    stage: ToolEventStage<'_>,
)

作用:把命令工具的某个阶段转换成具体事件。开始阶段发“开始”,结束或失败阶段整理成统一的“结束”事件。

数据流:输入是事件上下文、命令输入信息和阶段 → 如果是 Begin,它调用 emit_exec_command_begin;如果有输出,它取 stdout、stderr、聚合输出、退出码、耗时,并按退出码标成完成或失败;如果只有错误消息或被拒绝,它造一个没有正常输出的失败/拒绝结果 → 最后通常交给 emit_exec_end 发结束事件。

调用关系:ToolEmitter::emit 对命令类工具都会调用它。它是命令事件的中间转换层:上接 ToolEmitter,下接 emit_exec_command_begin 和 emit_exec_end。

调用图:调用 2 个内部函数(emit_exec_command_begin, emit_exec_end);被 1 处调用(emit);外部调用 2 个(new, format_exec_output_str)。

emit_exec_end536–564 ↗
async fn emit_exec_end(
    ctx: ToolEventCtx<'_>,
    exec_input: ExecCommandInput<'_>,
    exec_result: ExecCommandResult,
)

作用:发送“命令结束了”的事件,里面包含结果、耗时、退出码和最终状态。界面可以靠它显示命令是否成功,以及输出了什么。

数据流:输入是事件上下文、命令输入信息和整理好的执行结果 → 它补上完成时间戳,把命令、目录、输出、退出码、耗时、格式化输出和状态组装成 ExecCommandEndEvent → 通过 session 发出去。

调用关系:emit_exec_stage 在命令成功、失败、错误消息、被拒绝时都会调用它。它是命令类工具事件链条里的最后发报点。

调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(emit_exec_stage);外部调用 1 个(ExecCommandEnd)。

emit_patch_end566–618 ↗
async fn emit_patch_end(
    ctx: ToolEventCtx<'_>,
    changes: HashMap<PathBuf, FileChange>,
    stdout: String,
    stderr: String,
    status: PatchApplyStatus,
    tracker_update: TurnDiffTracker

作用:发送“补丁处理结束”的文件变更事件,并按需要更新本轮文件差异。它让用户不仅知道补丁成功或失败,还能看到最终代码差异。

数据流:输入是事件上下文、文件变化清单、标准输出、错误输出、补丁状态和差异跟踪指令 → 它先发送 FileChangeItem 完成事件 → 如果上下文里有差异跟踪器,就加锁读取和更新差异;必要时再发送 TurnDiffEvent,把最新 unified diff 发出去。

调用关系:ToolEmitter::emit 在 apply_patch 成功、失败、被拒绝时会调用它。测试也直接调用它来验证差异更新行为。它连接了“补丁结果展示”和“本轮文件差异追踪”两件事。

调用图:被 3 处调用(emit, invalidation_emits_empty_turn_diff, net_zero_patch_emits_empty_turn_diff);外部调用 2 个(FileChange, TurnDiff)。

tests::assert_failed_apply_patch_tracks_committed_delta636–695 ↗
async fn assert_failed_apply_patch_tracks_committed_delta(
        out: Result<ExecToolCallOutput, ToolError>,
        expected_status: PatchApplyStatus,
    )

作用:这是测试用的辅助函数,用来确认一种特殊情况:补丁最后失败了,但已经提交的那一部分改动仍然会被记录进本轮差异。这样可以防止界面漏掉真实发生过的文件改动。

数据流:输入是一个模拟的工具结果和期望的补丁状态 → 它创建测试会话、临时目录和差异跟踪器,先实际应用一个补丁拿到 delta,再调用 ToolEmitter::finish 模拟失败收尾 → 最后从事件通道里读事件,检查状态正确,并确认 diff 里包含新增文件和内容。

调用关系:denied_apply_patch_tracks_committed_delta 和 rejected_apply_patch_tracks_committed_delta 都复用它。它会创建 ToolEventCtx,并通过 ToolEmitter::finish 间接走到 emit_patch_end。

调用图:调用 4 个内部函数(make_session_and_context_with_dynamic_tools_and_rx, new, new, from_absolute_path);外部调用 9 个(new, from_secs, new, new, new, assert!, apply_patch, tempdir, timeout)。

tests::denied_apply_patch_tracks_committed_delta698–711 ↗
async fn denied_apply_patch_tracks_committed_delta()

作用:测试“沙箱拒绝补丁”时,如果补丁已经有部分改动被提交,系统仍然会记录这些改动。沙箱可以理解成安全隔离区,用来限制工具能做什么。

数据流:它构造一个退出码为 1 的输出,并包装成 SandboxErr::Denied 这类错误 → 把这个错误交给 assert_failed_apply_patch_tracks_committed_delta → 期望最终文件变更状态是 Failed,但已提交的 diff 仍然能被看到。

调用关系:它本身只准备一种错误场景,具体检查工作交给 tests::assert_failed_apply_patch_tracks_committed_delta。这个测试覆盖 ToolEmitter::finish 里“沙箱拒绝但有补丁 delta”的分支。

调用图:外部调用 5 个(new, default, assert_failed_apply_patch_tracks_committed_delta, Codex, Sandbox)。

tests::rejected_apply_patch_tracks_committed_delta714–720 ↗
async fn rejected_apply_patch_tracks_committed_delta()

作用:测试“用户拒绝补丁”时,如果已经有已知的提交片段,系统也会把这部分变化纳入本轮差异。这样拒绝事件不会把真实发生的文件变化藏起来。

数据流:它构造 ToolError::Rejected,消息是 rejected by user → 把它交给 assert_failed_apply_patch_tracks_committed_delta → 期望补丁状态显示为 Declined,也就是被拒绝,同时 diff 里仍包含已经提交的内容。

调用关系:它复用 tests::assert_failed_apply_patch_tracks_committed_delta。这个测试覆盖 ToolEmitter::finish 里用户拒绝 apply_patch 的分支。

调用图:外部调用 2 个(assert_failed_apply_patch_tracks_committed_delta, Rejected)。

tests::net_zero_patch_emits_empty_turn_diff723–773 ↗
async fn net_zero_patch_emits_empty_turn_diff()

作用:测试“先加文件、再删掉同一个文件”这种净效果为零的情况。最后没有实际文件差异时,系统应该发出空 diff,让外界知道差异已经被清掉了。

数据流:它创建测试会话、临时目录和差异跟踪器 → 依次应用两个补丁:先新增 a.txt,再删除 a.txt → 每次都调用 emit_patch_end 更新跟踪器并读取事件;第一次检查 diff 有新增内容,第二次检查 diff 变成空字符串。

调用关系:它直接测试 emit_patch_end 和差异跟踪器的配合。这个测试保证 TurnDiffEvent 不只会在有内容时发送,也会在原有差异被抵消后发送空结果。

调用图:调用 5 个内部函数(make_session_and_context_with_dynamic_tools_and_rx, new, emit_patch_end, new, from_absolute_path);外部调用 9 个(new, new, new, new, new, assert!, assert_eq!, apply_patch, tempdir)。

tests::invalidation_emits_empty_turn_diff776–814 ↗
async fn invalidation_emits_empty_turn_diff()

作用:测试差异跟踪器被标记为失效时,会向外发出空 diff。这样界面不会继续展示一份已经不可信的旧差异。

数据流:它先创建测试会话、临时目录和跟踪器,并手动记录一份已有补丁差异 → 然后调用 emit_patch_end,传入 TurnDiffTrackerUpdate::Invalidate → 最后读取事件,确认收到的 TurnDiffEvent 里的 unified_diff 是空字符串。

调用关系:它直接调用 emit_patch_end,专门覆盖差异失效这条路径。这个测试保证当系统无法继续准确追踪差异时,会用空 diff 主动清理外部显示。

调用图:调用 5 个内部函数(make_session_and_context_with_dynamic_tools_and_rx, new, emit_patch_end, new, from_absolute_path);外部调用 8 个(new, new, new, new, new, assert_eq!, apply_patch, tempdir)。

core/src/turn_diff_tracker.rs源码 ↗
domain_logic每次 apply_patch 成功后更新;在需要展示本回合改动时读取

这个文件像一个“本回合改动账本”。apply_patch 每成功改一次文件,它就把改动记下来:原来内容是什么、现在内容是什么、文件有没有新增、删除、改名。最后它把这些账目合成一份统一 diff(统一 diff 是 Git 常见的补丁文本格式,能显示哪些行被加了、删了)。如果补丁不是“精确补丁”(也就是不能可靠知道改动前后的完整内容),它会把自己标成无效,避免给出看似准确但其实可能错的结果。它还支持多个运行环境:同一个路径如果来自不同环境,会用环境名区分,防止混在一起。为了省时间,它会缓存已经渲染过的 diff;内容没变就不重新算。生成 diff 时还会算 Git blob 的 SHA-1 标识,用来让输出更像真正的 Git diff。

函数细节20
TrackedPath::new32–37 ↗
fn new(environment_id: &str, path: &Path) -> Self

作用:把“环境编号”和“文件路径”合成一个可追踪的路径对象。这样即使两个环境里有同名文件,也不会被误认为是同一个文件。

数据流:输入一个 environment_id 和一个普通路径 → 把环境编号复制成字符串,把路径复制成 PathBuf(可拥有的路径)→ 输出一个 TrackedPath,后面所有改动记录都用它当文件身份。

调用关系:apply_change 在处理每条补丁改动时会调用它,把补丁里的路径包装成带环境信息的路径;测试里也会直接用它来构造路径场景。

调用图:被 2 处调用(apply_change, large_rewrite_returns_promptly_and_preserves_exact_content);外部调用 1 个(to_path_buf)。

TurnDiffTracker::default64–77 ↗
fn default() -> Self

作用:创建一个空白、有效的改动追踪器。它一开始没有任何文件记录,也没有生成过 diff。

数据流:没有业务输入 → 初始化各种表:原始内容表、当前内容表、改名来源表、diff 缓存等 → 输出一个可以开始记录补丁的 TurnDiffTracker。

调用关系:TurnDiffTracker::new 会靠它生成默认实例;一些调用点在启动一次任务或测试场景时,也会用它得到干净的追踪器。

调用图:被 2 处调用(invocation, multi_agent_v2_request_user_input_rejects_subagent_threads);外部调用 2 个(new, new)。

TurnDiffTracker::new81–83 ↗
fn new() -> Self

作用:给外部使用的构造函数,用来新建一个改动追踪器。它比直接调 default 更直观。

数据流:没有输入 → 调用默认初始化逻辑 → 返回一个空的 TurnDiffTracker,准备接收后续补丁变化。

调用关系:很多运行流程和测试都会在一次 turn 开始前调用它;with_environment_display_roots 也先用它建出基础对象,再填入显示根目录。

调用图:被 30 处调用(fatal_tool_error_stops_turn_and_reports_error, guardian_allows_shell_command_additional_permissions_requests_past_policy_validation, guardian_allows_unified_exec_additional_permissions_requests_past_policy_validation, shell_command_allows_sticky_turn_permissions_without_inline_request_permissions_feature, strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_skip, rejects_escalated_permissions_when_policy_not_on_request, request_permissions_tool_rejects_unknown_environment_id, request_permissions_tool_resolves_relative_paths_against_selected_environment, test_tool_runtime, unified_exec_rejects_escalated_permissions_when_policy_not_on_request (+15 more));外部调用 1 个(default)。

TurnDiffTracker::with_environment_display_roots85–91 ↗
fn with_environment_display_roots(
        display_roots: impl IntoIterator<Item = (String, PathBuf)>,
    ) -> Self

作用:新建追踪器时顺便告诉它:每个环境的根目录在哪里。这样生成 diff 时能显示更短、更好读的相对路径。

数据流:输入一组“环境编号 → 根目录路径” → 先创建空追踪器,再把这些根目录保存起来 → 输出带路径显示规则的追踪器。

调用关系:run_turn 在真正跑一轮任务时会用它;相关测试也用它检查路径显示和多环境区分是否正确。

调用图:被 3 处调用(run_turn, tracker_with_root, tracks_same_absolute_path_across_multiple_environments);外部调用 2 个(into_iter, new)。

TurnDiffTracker::track_delta93–107 ↗
fn track_delta(&mut self, environment_id: &str, delta: &AppliedPatchDelta)

作用:接收一次已经成功应用的补丁变化,并把它合并进本回合的总改动里。这是追踪器的主要入口。

数据流:输入环境编号和一个 AppliedPatchDelta(一次补丁产生的变化集合)→ 如果追踪器已无效就不做事;如果补丁不精确,就调用 invalidate 放弃追踪;如果精确,就逐条交给 apply_change,再刷新整份 unified diff → 更新内部账本和最终 diff。

调用关系:外层补丁流程在每次 apply_patch 成功后会调用它;它把细节分发给 apply_change,最后让 refresh_unified_diff 重新汇总可展示结果。

调用图:调用 5 个内部函数(changes, is_exact, apply_change, invalidate, refresh_unified_diff)。

TurnDiffTracker::invalidate109–113 ↗
fn invalidate(&mut self)

作用:把追踪器标记为“不再可信”。当遇到无法精确知道前后内容的补丁时,用它防止输出错误 diff。

数据流:没有额外输入 → 把 valid 设为 false,清空已缓存的 diff 和最终 unified diff → 之后 track_delta 再收到变化也会直接跳过。

调用关系:track_delta 发现 delta 不是精确变化时会调用它;它相当于踩刹车,告诉后续流程不要再相信这份改动账本。

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

TurnDiffTracker::get_unified_diff115–117 ↗
fn get_unified_diff(&self) -> Option<String>

作用:取出当前已经汇总好的统一 diff 文本。调用者用它来展示本回合到底改了什么。

数据流:读取内部 unified_diff → 克隆一份 Option<String> 返回;如果没有改动或追踪器无效,就返回 None。

调用关系:它是对外读取结果的窗口;前面的 track_delta 和 refresh_unified_diff 负责更新内容,它只负责把现成结果交出去。

TurnDiffTracker::has_unified_diff119–121 ↗
fn has_unified_diff(&self) -> bool

作用:快速回答“现在有没有可展示的 diff”。它不拿出完整文本,只给一个是或否。

数据流:读取内部 unified_diff 是否存在 → 返回布尔值;内部状态不被改动。

调用关系:同一模块内的调用者可以用它做轻量判断,比如决定是否需要展示或附带改动摘要。

TurnDiffTracker::refresh_unified_diff123–183 ↗
fn refresh_unified_diff(&mut self)

作用:把当前记录的所有文件变化重新整理成一整份统一 diff。它负责把新增、删除、修改、改名这些账目排好顺序并合并输出。

数据流:读取原始内容表、当前内容表、改名来源表和旧的 diff 缓存 → 找出改名配对,收集涉及的路径,按显示路径排序;对每个文件复用缓存或重新渲染 diff;把所有片段拼成一个大字符串 → 更新 rendered_diffs 缓存和 unified_diff 结果。

调用关系:track_delta 在应用完一批补丁后会调用它;它会先问 rename_pairs 哪些文件是改名,再在需要时生成单文件 diff。

调用图:调用 1 个内部函数(rename_pairs);被 1 处调用(track_delta);外部调用 4 个(new, new, new, take)。

TurnDiffTracker::apply_change185–211 ↗
fn apply_change(&mut self, environment_id: &str, change: &AppliedPatchChange)

作用:把一条补丁里的具体文件变化分流到正确处理方式。它像前台分诊:新增、删除、更新/改名分别走不同通道。

数据流:输入环境编号和一条 AppliedPatchChange → 先把补丁路径包装成 TrackedPath → 根据变化类型调用 apply_add、apply_delete 或 apply_update → 内部的原始/当前内容记录被更新。

调用关系:track_delta 会对 delta 里的每条 change 调用它;它不直接生成 diff,只负责把账本改对。

调用图:调用 4 个内部函数(new, apply_add, apply_delete, apply_update);被 1 处调用(track_delta)。

TurnDiffTracker::apply_add213–225 ↗
fn apply_add(&mut self, path: TrackedPath, content: &str, overwritten_content: Option<&str>)

作用:记录一个文件被新增,或者一个原本存在的文件被新增内容覆盖。它会保存“之前是什么”和“现在是什么”。

数据流:输入目标路径、新内容,以及可选的被覆盖旧内容 → 清掉这个路径的旧改名来源;如果这是第一次见到该路径且有被覆盖内容,就把被覆盖内容记为 baseline;再把新内容记为 current → 这个路径在账本里变成“现在有这些内容”。

调用关系:apply_change 遇到 Add 类型变化时调用它;它会通过 tracked_content 给内容打上版本号,方便后面缓存 diff 时判断内容有没有变。

调用图:调用 1 个内部函数(tracked_content);被 1 处调用(apply_change);外部调用 1 个(clone)。

TurnDiffTracker::apply_delete227–235 ↗
fn apply_delete(&mut self, path: TrackedPath, content: &str)

作用:记录一个文件被删除。它确保账本里知道删除前的内容,否则无法生成“删了哪些行”的 diff。

数据流:输入路径和删除前内容 → 如果当前内容表里有这个文件,就移除它;如果之前从没见过这个文件,就把删除前内容记为 baseline;同时清掉改名来源记录 → 结果是这个路径只剩“原来有,现在没了”。

调用关系:apply_change 遇到 Delete 类型变化时调用它;它也会用 tracked_content 保存删除前内容并分配版本号。

调用图:调用 1 个内部函数(tracked_content);被 1 处调用(apply_change);外部调用 1 个(clone)。

TurnDiffTracker::apply_update237–280 ↗
fn apply_update(
        &mut self,
        source_path: TrackedPath,
        move_path: Option<TrackedPath>,
        old_content: &str,
        overwritten_move_content: Option<&str>,
        new_con

作用:记录文件内容更新,也处理“更新同时改名”的情况。它是最复杂的记账逻辑,因为要弄清楚文件从哪里来、最后到哪里去。

数据流:输入源路径、可选目标路径、旧内容、可选的目标处被覆盖内容、新内容 → 如果源文件第一次出现,就把旧内容记为 baseline;如果发生移动,必要时记录目标原本内容,移除源路径当前内容,把新内容放到目标路径,并维护“目标来自哪个源文件”的关系;如果不移动,就直接更新源路径当前内容 → 最终账本反映修改或改名后的状态。

调用关系:apply_change 遇到 Update 类型变化时调用它;它依赖 tracked_content 给每份内容编号,并维护 origin_by_current_path,后面 rename_pairs 会用这些信息判断是否该显示成改名。

调用图:调用 1 个内部函数(tracked_content);被 1 处调用(apply_change);外部调用 1 个(clone)。

TurnDiffTracker::tracked_content282–289 ↗
fn tracked_content(&mut self, content: &str) -> TrackedContent

作用:把一段文本包装成可追踪内容,并给它一个递增版本号。版本号用来判断缓存的 diff 还能不能复用。

数据流:输入文本内容 → 读取当前 next_revision 作为版本号,然后把 next_revision 加一 → 输出 TrackedContent,里面包含文本副本和版本号。

调用关系:apply_add、apply_delete、apply_update 在保存任何文件内容时都会调用它;它让 refresh_unified_diff 能通过版本号识别内容变化。

调用图:被 3 处调用(apply_add, apply_delete, apply_update)。

TurnDiffTracker::rename_pairs291–307 ↗
fn rename_pairs(&self) -> HashMap<TrackedPath, TrackedPath>

作用:找出哪些变化应该被看成“文件改名”。这样 diff 可以显示从旧路径到新路径,而不是误报成一个删除加一个新增。

数据流:读取 origin_by_current_path、baseline_by_path 和 current_by_path → 过滤掉不成立的情况,比如源文件还存在、目标文件不存在、目标原来就有 baseline 等 → 输出“原路径 → 新路径”的配对表。

调用关系:refresh_unified_diff 在整理总 diff 前会调用它;它提供改名线索,让后续渲染能把一对路径当成同一个文件的前后状态。

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

TurnDiffTracker::render_diff309–366 ↗
fn render_diff(
        &self,
        left_path: &TrackedPath,
        left_content: Option<&str>,
        right_path: &TrackedPath,
        right_content: Option<&str>,
    ) -> Option<String>

作用:为单个文件生成 Git 风格的 diff 片段。它能处理新增、删除、普通修改和改名后的路径显示。

数据流:输入左边路径和旧内容、右边路径和新内容;内容可以为空,表示文件不存在 → 如果两边内容一样就返回 None;否则计算显示路径和 Git blob 标识,写出 diff 头部,再用 similar 库按行比较文本,最多等待 100 毫秒 → 输出一个 diff 字符串。

调用关系:refresh_unified_diff 在需要单文件 diff 时使用它;它会调用 display_path 生成用户看得懂的路径,并通过 git_blob_oid 间接得到 Git 风格的内容编号。

调用图:调用 1 个内部函数(display_path);外部调用 2 个(format!, configure)。

TurnDiffTracker::rendered_diff_count369–371 ↗
fn rendered_diff_count(&self) -> usize

作用:测试专用:返回真正渲染 diff 的次数。它用来确认缓存是否起作用。

数据流:读取测试环境下的 rendered_diff_count 计数器 → 返回数字;不改变正常业务状态。

调用关系:只在测试编译时存在;测试用它观察 render_diff 是否被重复调用,从而验证缓存逻辑。

TurnDiffTracker::display_path373–385 ↗
fn display_path(&self, path: &TrackedPath) -> String

作用:把内部保存的真实路径变成 diff 里显示的路径。它会尽量去掉环境根目录,让路径短一些,也会处理多环境前缀。

数据流:输入一个 TrackedPath → 如果这个环境配置了显示根目录,就尝试把根目录前缀去掉;把反斜杠换成斜杠,保证跨平台显示一致;如果有多个环境且环境名不空,就在前面加环境编号 → 输出最终显示用路径字符串。

调用关系:render_diff 生成 diff 头部时调用它;它让底层文件路径变成用户更容易读、也不会跨环境混淆的形式。

调用图:被 1 处调用(render_diff);外部调用 1 个(format!)。

git_blob_oid388–390 ↗
fn git_blob_oid(data: &[u8]) -> String

作用:把一段文件内容算成 Git 使用的 blob 对象 ID 字符串。blob 可以简单理解成 Git 里“某份文件内容”的编号。

数据流:输入字节数据 → 调用 git_blob_sha1_hex_bytes 算出 Git blob 的 SHA-1 哈希值 → 格式化成十六进制字符串返回。

调用关系:render_diff 生成 index 行时需要旧内容和新内容的编号,会通过它得到看起来和 Git diff 一样的对象 ID。

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

git_blob_sha1_hex_bytes393–400 ↗
fn git_blob_sha1_hex_bytes(data: &[u8]) -> Output<sha1::Sha1>

作用:按 Git 的规则计算文件内容的 SHA-1。注意它不是直接 hash 文件内容,而是先加上 Git 要求的 blob 头。

数据流:输入原始字节 → 先生成类似“blob 长度\0”的头部,再把头部和内容一起喂给 SHA-1 算法 → 输出固定长度的哈希结果字节。

调用关系:git_blob_oid 调用它完成真正的计算;它保证 diff 里的 index 编号和 Git 对同样内容算出来的编号一致。

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

core/src/agent/status.rs源码 ↗
domain_logiccross-cutting

系统里的智能体会不断吐出事件,比如“开始一轮工作了”“这一轮完成了”“被用户打断了”“报错了”。但别的代码通常不想直接理解这些零散事件,而是想知道一个简单结论:现在到底算什么状态。这个文件就像一个状态翻译员,把事件换成统一的 AgentStatus(智能体状态)。如果事件和状态无关,它就返回空,表示不用更新。它还提供了一个小判断:某个状态是不是“最终状态”。这里的最终状态就是后面不会再继续跑的状态,比如完成、出错、关机;而等待初始化、运行中、被打断还不算彻底结束。这个判断会被很多等待任务结束、恢复任务、通知上层的地方使用,避免系统太早下结论,也避免一直等一个已经结束的任务。

函数细节2
agent_status_from_event6–21 ↗
fn agent_status_from_event(msg: &EventMsg) -> Option<AgentStatus>

作用:把一个事件消息转换成对应的智能体状态。有人拿到事件后,可以用它判断是否需要更新界面、通知外部,或者记录任务结果。

数据流:输入是一条 EventMsg,也就是智能体刚发出来的一条事件。函数查看事件类型:如果是开始,就变成 Running;如果是完成,就变成 Completed,并带上最后一条智能体回复;如果是中断或预算耗尽,就变成 Interrupted;如果是其他异常中止或错误,就变成 Errored,并带上错误原因;如果是关机完成,就变成 Shutdown。遇到和状态无关的事件,就输出 None,表示状态不用变。

调用关系:它处在“事件流”和“状态记录”之间,作用是把零散事件整理成稳定状态。内部会构造 Completed 和 Errored 这类状态;遇到不能简单归类的中止原因时,会用格式化文本把原因写进错误状态,方便后面显示或记录。

调用图:外部调用 3 个(format!, Completed, Errored)。

is_final23–28 ↗
fn is_final(status: &AgentStatus) -> bool

作用:判断一个智能体状态是不是已经走到终点。它帮调用方分清“还可能继续变化”和“已经可以收尾”。

数据流:输入是一个 AgentStatus。函数检查它是不是 PendingInit、Running 或 Interrupted:这三种都表示还不能当作彻底结束;除此之外都算最终状态。输出是一个布尔值,true 表示已经结束,false 表示还没结束或还可能继续。

调用关系:它被多个收尾和等待流程使用,比如 maybe_start_completion_watcher、maybe_notify_parent_of_terminal_turn、find_finished_threads、recover_running_items、handle_call 和 wait_for_final_status。这些地方需要决定是否启动完成监听、是否通知父任务、是否找出已结束线程、是否恢复运行中项目,或者是否继续等待最终状态。它本身不做收尾,只提供一个可靠的“是否结束”判断。

调用图:被 6 处调用(maybe_start_completion_watcher, maybe_notify_parent_of_terminal_turn, find_finished_threads, recover_running_items, handle_call, wait_for_final_status);外部调用 1 个(matches!)。

core/src/event_mapping.rs源码 ↗
domain_logicconversation event mapping

模型返回的数据很杂:有用户消息、助手回复、推理摘要、网页搜索、图片生成,还有一些系统偷偷塞进去的上下文说明。这个文件就像一个“翻译和分拣台”,先判断一段内容是不是临时上下文,再把真正该进入对话历史的东西转成内部的 TurnItem。用户消息会被转成文字或图片输入;助手消息会被转成助手文字;推理、网页搜索、图片生成也会各自变成专门的条目。一个重要细节是,图片前后的占位标签会被跳过,因为真正有用的是图片本身,不是包住图片的标记文字。这样后面的代码看到的就是干净、统一的对话记录。

函数细节7
is_contextual_user_message_content38–40 ↗
fn is_contextual_user_message_content(message: &[ContentItem]) -> bool

作用:判断一条用户消息里是否包含“上下文片段”。这里的上下文片段不是用户真正说的话,而是系统为了让模型理解当前环境临时塞进去的提示。

数据流:输入是一组消息内容片段 → 它逐个查看这些片段,看有没有片段被识别为上下文 → 输出 true 或 false,不改动原消息。

调用关系:它是很多地方的过滤器。裁剪回滚上下文、判断用户回合边界、收集对话记录、构建当前线程说明时都会用它;parse_user_message 也先问它一句,如果是上下文消息,就不把它当成普通用户发言。

调用图:被 5 处调用(trim_pre_turn_context_updates, is_user_turn_boundary, parse_user_message, collect_guardian_transcript_entries, build_current_thread_section);外部调用 1 个(iter)。

is_contextual_dev_message_content47–49 ↗
fn is_contextual_dev_message_content(message: &[ContentItem]) -> bool

作用:判断开发者消息里是否含有可以在回滚时裁掉的上下文提示。开发者消息可以理解为给模型的额外规则或环境说明。

数据流:输入是一组开发者消息内容片段 → 它逐个检查有没有符合“上下文前缀”的片段 → 输出 true 或 false,本身不修改任何内容。

调用关系:它主要服务于 trim_pre_turn_context_updates。那段流程需要知道哪些开发者内容只是临时上下文,方便在回滚或重建对话基线时正确处理。

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

has_non_contextual_dev_message_content53–57 ↗
fn has_non_contextual_dev_message_content(message: &[ContentItem]) -> bool

作用:判断开发者消息里是否有真正持久的内容,而不只是临时上下文提示。这样系统能分清“要保留的规则”和“可以裁掉的环境说明”。

数据流:输入是一组开发者消息内容片段 → 它查找是否存在任何一个片段不属于上下文片段 → 输出 true 或 false,不改变输入。

调用关系:它和 is_contextual_dev_message_content 常常配套使用。trim_pre_turn_context_updates 用它来避免误删开发者消息里真正有意义、应该继续保留的部分。

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

is_contextual_dev_fragment59–70 ↗
fn is_contextual_dev_fragment(content_item: &ContentItem) -> bool

作用:判断单个开发者内容片段是不是系统认可的上下文提示。它看的不是整条消息,而是其中一小块文字。

数据流:输入是一个内容片段 → 如果它不是输入文字,就直接判定不是上下文;如果是文字,就去掉开头空白,再看它是否以特定标签开头,比较时不区分英文大小写 → 输出 true 或 false。

调用关系:它是开发者消息判断里的小尺子。is_contextual_dev_message_content 用它找“有没有上下文片段”,has_non_contextual_dev_message_content 用它反过来找“有没有非上下文片段”。

parse_user_message72–109 ↗
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem>

作用:把模型格式里的用户消息转成系统内部的 UserMessageItem。它会跳过纯上下文消息,避免把系统提示误显示成用户发言。

数据流:输入是一组内容片段 → 先检查它是不是上下文消息,是的话返回空;否则逐个片段转换:文字变成用户文字输入,图片变成用户图片输入,紧挨着图片的图片开关标签会被忽略;如果用户消息里混进了输出文字,就记录警告 → 最后输出一个 UserMessageItem,或在应跳过时输出 None。

调用关系:它由 parse_turn_item 在遇到 role 为 user 的消息时调用。在调用它之前,parse_turn_item 还会先尝试把消息解析成可见的 hook prompt;如果不是 hook prompt,才交给它生成普通用户消息。

调用图:调用 6 个内部函数(is_contextual_user_message_content, new, is_image_close_tag_text, is_image_open_tag_text, is_local_image_close_tag_text, is_local_image_open_tag_text);外部调用 4 个(new, matches!, iter, warn!)。

parse_agent_message111–137 ↗
fn parse_agent_message(
    id: Option<&String>,
    message: &[ContentItem],
    phase: Option<MessagePhase>,
) -> AgentMessageItem

作用:把模型格式里的助手消息转成系统内部的 AgentMessageItem。它让后面的代码不用关心原始消息里文字字段的具体长相。

数据流:输入是可选消息 id、一组内容片段、可选阶段信息 → 它把输入文字和输出文字都收集成助手文本内容;如果遇到不该出现在助手消息里的内容,比如图片输入,就记一条警告;如果没有 id,就新生成一个随机 id → 输出完整的 AgentMessageItem。

调用关系:它只在 parse_turn_item 解析 assistant 角色消息时使用。parse_turn_item 负责认出“这是助手消息”,然后把具体转换工作交给它。

调用图:被 1 处调用(parse_turn_item);外部调用 3 个(new, iter, warn!)。

parse_turn_item139–214 ↗
fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem>

作用:把一个原始 ResponseItem 翻译成系统内部统一的 TurnItem。TurnItem 可以理解为对话时间线上的一格:可能是用户说话、助手回复、推理、网页搜索或图片生成。

数据流:输入是一个模型返回的 ResponseItem → 它按类型分流:消息按角色区分用户、助手、系统;用户消息先尝试解析成 hook prompt,再尝试普通用户消息;助手消息交给 parse_agent_message;推理会提取摘要和原文;网页搜索会提取动作和查询;图片生成会保留状态、改写提示和结果 → 输出对应的 TurnItem,遇到系统消息或不关心的类型就返回 None。

调用关系:它是这个文件对外最核心的转换入口。insert_initial_context_before_last_real_user_or_summary 在整理初始上下文时会调用它;它内部再把用户、助手、网页搜索等具体转换分派给 parse_visible_hook_prompt_message、parse_agent_message 和 web_search_action_detail 等函数。

调用图:调用 2 个内部函数(parse_agent_message, web_search_action_detail);被 1 处调用(insert_initial_context_before_last_real_user_or_summary);外部调用 6 个(new, AgentMessage, ImageGeneration, Reasoning, WebSearch, parse_visible_hook_prompt_message)。

core/src/stream_events_utils.rs源码 ↗
orchestrationrequest handling

可以把这个文件想成“收件分拣员”。模型输出一个完成项后,它先判断这是普通回答、思考内容、联网搜索、生成图片,还是一次工具调用。普通内容会被转成对话记录,并去掉用户不该直接看到的隐藏标记,比如引用标记和计划块。工具调用会被立刻记录下来,再交给工具运行器异步执行。图片结果会从 Base64(一种把二进制图片写成文本的编码)还原成 png 文件,放到固定目录里。它还会检查回答里有没有“记忆引用”,把用过哪些记忆记进数据库。另一个重要细节是“邮箱投递”:如果助手给出了正式回复,某些后续消息会被推迟到下一轮,避免当前轮被突然插话打乱。

函数细节21
image_generation_artifact_path42–68 ↗
fn image_generation_artifact_path(
    codex_home: &AbsolutePathBuf,
    session_id: &str,
    call_id: &str,
) -> AbsolutePathBuf

作用:给一张生成出来的图片算出应该保存到哪里。它会把会话编号和调用编号里的奇怪字符替换掉,避免生成不安全或不好用的文件名。

数据流:进去的是 Codex 主目录、会话编号和图片调用编号 → 函数把编号清洗成只含字母数字、横线和下划线的名字 → 出来的是一个形如 generated_images/会话/调用.png 的完整路径,不写文件,只负责算地址。

调用关系:保存图片的 save_image_generation_result 会用它决定落盘位置;persist_image_generation_item 和 record_image_generation_instructions 也用它保持真实保存路径和提示用户的路径一致;相关测试会检查图片历史和发布流程是否按这个路径工作。

调用图:调用 1 个内部函数(join);被 6 处调用(handle_output_item_done_records_image_save_history_message, handle_output_item_done_skips_image_save_message_when_save_fails, persist_image_generation_item, record_image_generation_instructions, save_image_generation_result, image_generation_publication_is_finalized_by_core);外部调用 1 个(format!)。

strip_hidden_assistant_markup70–77 ↗
fn strip_hidden_assistant_markup(text: &str, plan_mode: bool) -> String

作用:把助手文本里给机器看的隐藏标记去掉,只留下适合展示或当作最后回复的文字。计划模式下还会去掉“拟定计划”块。

数据流:进去的是一段助手文本和是否处于计划模式 → 先移除引用标记,再按需要移除计划块 → 出来的是更干净、用户更容易读的文本。

调用关系:last_assistant_message_from_item 会调用它来判断一条模型消息是否能算作真正的助手回复;它依赖外部的 strip_citations 和 strip_proposed_plan_blocks 做具体清理。

调用图:被 1 处调用(last_assistant_message_from_item);外部调用 2 个(strip_citations, strip_proposed_plan_blocks)。

strip_hidden_assistant_markup_and_parse_memory_citation79–93 ↗
fn strip_hidden_assistant_markup_and_parse_memory_citation(
    text: &str,
    plan_mode: bool,
) -> (
    String,
    Option<codex_protocol::memory_citation::MemoryCitation>,
)

作用:一边清理助手文本,一边把隐藏在引用里的“记忆引用”读出来。这样既能给用户看干净文字,又能让系统知道这次回答用到了哪些记忆。

数据流:进去的是原始文本和计划模式标志 → 先拆出引用,再生成可见文本,同时尝试把引用解析成 MemoryCitation(记忆引用记录) → 出来的是清理后的文本和可选的记忆引用。

调用关系:finalize_turn_item 在最终整理助手消息时调用它;它把 strip_citations、strip_proposed_plan_blocks 和 parse_memory_citation 串起来,分别完成去标记、去计划块、识别记忆。

调用图:调用 1 个内部函数(parse_memory_citation);被 1 处调用(finalize_turn_item);外部调用 2 个(strip_citations, strip_proposed_plan_blocks)。

raw_assistant_output_text_from_item95–109 ↗
fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option<String>

作用:从一个模型输出项里拿出助手真正说的原始文字。它只认 role 是 assistant 的消息,其他类型会忽略。

数据流:进去的是 ResponseItem(模型输出项) → 如果它是助手消息,就把里面所有 OutputText 文本片段按顺序拼起来 → 出来的是拼好的字符串;如果不是助手文字,出来就是 None。

调用关系:try_run_sampling_request、last_assistant_message_from_item、record_stage1_output_usage_and_detect_memory_citation 和 response_item_records_turn_ttft 都会用它拿原始助手文本;它是后续清理、统计和引用检测的入口。

调用图:被 4 处调用(try_run_sampling_request, last_assistant_message_from_item, record_stage1_output_usage_and_detect_memory_citation, response_item_records_turn_ttft)。

save_image_generation_result111–128 ↗
async fn save_image_generation_result(
    codex_home: &AbsolutePathBuf,
    session_id: &str,
    call_id: &str,
    result: &str,
) -> Result<AbsolutePathBuf>

作用:把图片生成接口返回的文本结果真正保存成图片文件。这里的结果是 Base64 编码的图片内容,需要先解码再写到磁盘。

数据流:进去的是主目录、会话编号、图片调用编号和 Base64 图片文本 → 函数解码成字节,算出保存路径,创建缺失的文件夹,再写入 png 文件 → 成功时出来保存路径,失败时出来错误。

调用关系:persist_image_generation_item 调用它完成真正的落盘;它内部用 image_generation_artifact_path 统一路径规则,并调用异步文件系统创建目录和写文件。

调用图:调用 1 个内部函数(image_generation_artifact_path);被 1 处调用(persist_image_generation_item);外部调用 2 个(create_dir_all, write)。

persist_image_generation_item130–166 ↗
async fn persist_image_generation_item(
    sess: &Session,
    turn_context: &TurnContext,
    image_item: &mut ImageGenerationItem,
) -> Option<AbsolutePathBuf>

作用:把一个已经完成的图片生成结果保存起来,并把保存路径写回这条图片记录。保存失败不会让整轮对话崩掉,只会记一条警告。

数据流:进去的是当前会话、当前轮上下文和可修改的 ImageGenerationItem → 先清空旧保存路径,再调用 save_image_generation_result → 成功后把 saved_path 填上并返回路径;失败后记录警告并返回 None。

调用关系:finalize_turn_item 在发现图片生成项已经 completed 时调用它;它依赖 save_image_generation_result 做实际写文件,失败时用 image_generation_artifact_path 算出预期目录用于日志说明。

调用图:调用 2 个内部函数(image_generation_artifact_path, save_image_generation_result);被 1 处调用(finalize_turn_item);外部调用 1 个(warn!)。

record_image_generation_instructions168–188 ↗
async fn record_image_generation_instructions(
    sess: &Session,
    turn_context: &TurnContext,
    image_item: &ImageGenerationItem,
)

作用:在图片保存成功后,往对话历史里补一条说明,告诉后续模型图片会被放在哪个目录、路径长什么样。这样模型之后能正确引用或说明生成图片的位置。

数据流:进去的是会话、当前轮上下文和图片项 → 如果图片没有保存路径就直接结束;否则算出图片目录和示例路径,构造一段上下文用户片段 → 最后把这段提示写入对话记录。

调用关系:handle_non_tool_response_item 在整理图片生成项后调用它;它使用 image_generation_artifact_path 生成路径,并通过 record_conversation_items 把说明塞进会话历史。

调用图:调用 3 个内部函数(into, new, image_generation_artifact_path);被 1 处调用(handle_non_tool_response_item);外部调用 1 个(record_conversation_items)。

record_completed_response_item191–203 ↗
async fn record_completed_response_item(
    sess: &Session,
    turn_context: &TurnContext,
    item: &ResponseItem,
)

作用:把一条已经完成的模型输出写进会话历史,并触发和记忆、邮箱投递有关的附加记录。它是更完整函数的简化入口。

数据流:进去的是会话、当前轮上下文和输出项 → 它不自己分析细节,而是把 finalized_facts 设为空交给 record_completed_response_item_with_finalized_facts → 出来没有返回值,但会更新历史和相关状态。

调用关系:handle_output_item_done 在工具调用、工具拒绝等路径里会调用它;真正工作交给 record_completed_response_item_with_finalized_facts,避免两处逻辑不一致。

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

record_completed_response_item_with_finalized_facts205–244 ↗
async fn record_completed_response_item_with_finalized_facts(
    sess: &Session,
    turn_context: &TurnContext,
    item: &ResponseItem,
    finalized_facts: Option<&FinalizedTurnItemFacts>,
)

作用:这是保存完成输出项的“总账房”。它记录对话项,决定是否把邮箱消息推迟到下一轮,还会登记这次回答是否引用了记忆。

数据流:进去的是会话、当前轮上下文、输出项,以及可选的已整理事实 → 先把输出项写入历史;再判断是否要延后邮箱投递;然后检查外部上下文是否会污染记忆模式;最后检测或使用已有的记忆引用并写入数据库 → 出来没有直接结果,但会改变会话历史、输入队列和记忆使用记录。

调用关系:record_completed_response_item 只是包一层调用它;handle_output_item_done 和计划模式下的 handle_assistant_item_done_in_plan_mode 会在输出完成时调用它;它把记忆污染、引用记录和邮箱投递这几件后续事务串在一起。

调用图:调用 3 个内部函数(mark_thread_memory_mode_polluted_if_external_context, record_stage1_output_usage_and_detect_memory_citation, record_stage1_output_usage_for_memory_citation);被 3 处调用(handle_assistant_item_done_in_plan_mode, handle_output_item_done, record_completed_response_item);外部调用 3 个(record_conversation_items, record_memory_citation_for_turn, from_ref)。

response_item_may_include_external_context246–253 ↗
fn response_item_may_include_external_context(item: &ResponseItem) -> bool

作用:判断一个输出项是否可能包含外部上下文,比如搜索工具或网页搜索带来的内容。这个判断用于保护记忆模式,避免混入外部资料后还误以为全是用户自己的记忆。

数据流:进去的是一个 ResponseItem → 函数检查它是不是工具搜索调用、工具搜索输出或网页搜索调用 → 出来是 true 或 false。

调用关系:mark_thread_memory_mode_polluted_if_external_context 会先问它这个输出项有没有外部上下文风险;它本身只是一个小判断,不改任何状态。

调用图:被 1 处调用(mark_thread_memory_mode_polluted_if_external_context);外部调用 1 个(matches!)。

mark_thread_memory_mode_polluted_if_external_context255–271 ↗
async fn mark_thread_memory_mode_polluted_if_external_context(
    sess: &Session,
    turn_context: &TurnContext,
    item: &ResponseItem,
)

作用:当配置要求“遇到外部上下文就禁用或标脏记忆模式”时,它负责给当前线程打上被外部资料污染的标记。这样系统之后不会把搜索来的东西误当成纯记忆依据。

数据流:进去的是会话、当前轮上下文和输出项 → 先看配置是否开启这个保护,再看输出项是否可能含外部上下文 → 条件满足时写入状态数据库,把当前线程标记为 memory mode polluted。

调用关系:record_completed_response_item_with_finalized_facts 在保存完成输出时调用它;drain_in_flight 也可能在处理未完成任务时调用它;它用 response_item_may_include_external_context 做前置判断,再把标记动作交给 state_db。

调用图:调用 2 个内部函数(response_item_may_include_external_context, mark_thread_memory_mode_polluted);被 2 处调用(drain_in_flight, record_completed_response_item_with_finalized_facts)。

record_stage1_output_usage_and_detect_memory_citation273–286 ↗
async fn record_stage1_output_usage_and_detect_memory_citation(
    state_db_ctx: Option<&state_db::StateDbHandle>,
    item: &ResponseItem,
) -> bool

作用:从助手原始回答里找记忆引用,并登记这些记忆确实被第一阶段输出用过。简单说,就是给“这次回答用了哪些记忆”做记账。

数据流:进去的是可选的状态数据库句柄和输出项 → 先提取助手原始文字,再拆出引用并尝试解析成记忆引用 → 如果找到,就交给 record_stage1_output_usage_for_memory_citation 记录;出来是是否发现记忆引用。

调用关系:record_completed_response_item_with_finalized_facts 在没有现成 finalized facts 时调用它;它依赖 raw_assistant_output_text_from_item 拿文本,依赖 strip_citations 和 parse_memory_citation 识别引用。

调用图:调用 3 个内部函数(raw_assistant_output_text_from_item, record_stage1_output_usage_for_memory_citation, parse_memory_citation);被 1 处调用(record_completed_response_item_with_finalized_facts);外部调用 1 个(strip_citations)。

record_stage1_output_usage_for_memory_citation288–301 ↗
async fn record_stage1_output_usage_for_memory_citation(
    state_db_ctx: Option<&state_db::StateDbHandle>,
    memory_citation: &MemoryCitation,
) -> bool

作用:拿到已经解析好的记忆引用后,把其中涉及的线程编号写入数据库,表示这些记忆被本次输出使用过。

数据流:进去的是可选状态数据库和 MemoryCitation → 先从引用里提取线程编号 → 如果没有编号,也返回 true 表示确实有引用;如果有数据库,就尝试记录这些编号 → 出来是是否存在有效记忆引用。

调用关系:record_stage1_output_usage_and_detect_memory_citation 找到引用后会调用它;record_completed_response_item_with_finalized_facts 如果已经有 finalized facts,也会直接调用它;它把具体线程编号解析交给 thread_ids_from_memory_citation。

调用图:调用 1 个内部函数(thread_ids_from_memory_citation);被 2 处调用(record_completed_response_item_with_finalized_facts, record_stage1_output_usage_and_detect_memory_citation)。

apply_turn_item_contributors324–338 ↗
async fn apply_turn_item_contributors(
    sess: &Session,
    turn_store: &ExtensionData,
    item: &mut TurnItem,
)

作用:让扩展插件有机会给一个 TurnItem(对话轮里的展示项)补充信息。就像文件归档前让各个部门盖章,但某个部门失败不会阻止归档。

数据流:进去的是会话、当前轮的扩展数据仓库和可修改的 TurnItem → 函数逐个调用已注册的 contributor → 每个 contributor 可以修改这个 TurnItem;失败只写警告,不中断整个流程。

调用关系:finalize_turn_item 在策略允许时调用它;它从 sess.services.extensions 取出贡献者,再把线程级和本轮级扩展数据交给这些贡献者。

调用图:被 1 处调用(finalize_turn_item);外部调用 1 个(warn!)。

finalize_non_tool_response_item357–402 ↗
async fn finalize_non_tool_response_item(
    sess: &Session,
    turn_context: &TurnContext,
    contributor_policy: TurnItemContributorPolicy<'_>,
    item: &ResponseItem,
    plan_mode: bool,
) ->

作用:把一个非工具调用的模型输出整理成最终的 TurnItem,并顺手提炼出几个后面要用的事实,比如最后一句助手消息、是否有记忆引用、是否要延后邮箱投递。

数据流:进去的是会话、上下文、插件执行策略、输出项和计划模式标志 → 先调用 handle_non_tool_response_item 转成 TurnItem 并做清理 → 再根据 TurnItem 类型提取 memory_citation、last_agent_message 和 defers_mailbox_delivery_to_next_turn → 出来是 FinalizedTurnItem,里面有整理好的展示项和事实。

调用关系:handle_output_item_done 在普通输出路径会调用它;计划模式相关的 handle_assistant_item_done_in_plan_mode 也会用它;它把真正的转换工作交给 handle_non_tool_response_item,自己负责总结后续记录所需的信息。

调用图:调用 1 个内部函数(handle_non_tool_response_item);被 2 处调用(handle_assistant_item_done_in_plan_mode, handle_output_item_done);外部调用 1 个(matches!)。

handle_output_item_done405–515 ↗
async fn handle_output_item_done(
    ctx: &mut HandleOutputCtx,
    item: ResponseItem,
    previously_active_item: Option<TurnItem>,
) -> Result<OutputItemResult>

作用:这是模型输出项完成时的核心分拣入口。它决定这条输出是要启动工具、展示给用户、保存到历史,还是把错误返回给模型。

数据流:进去的是可修改的 HandleOutputCtx、一个完成的 ResponseItem,以及可选的上一条活跃 TurnItem → 先尝试把它识别成工具调用;如果是工具调用,就记录并启动异步工具任务;如果不是,就转成 TurnItem、发出开始和完成事件、保存历史;如果工具请求需要直接答复模型,就构造函数调用输出并写入历史; fatal 错误则返回错误 → 出来是 OutputItemResult,告诉外层是否要跟进、有没有工具任务、最后助手消息是什么。

调用关系:try_run_sampling_request 在读取模型流时调用它;它把工作分发给 ToolRouter::build_tool_call、ToolCallRuntime、finalize_non_tool_response_item、record_completed_response_item_with_finalized_facts 和 response_input_to_response_item,是这一文件最重要的调度点。

调用图:调用 5 个内部函数(finalize_non_tool_response_item, record_completed_response_item, record_completed_response_item_with_finalized_facts, response_input_to_response_item, build_tool_call);被 1 处调用(try_run_sampling_request);外部调用 10 个(pin, default, new, default, Run, Fatal, Text, clone, from_ref, info!)。

handle_non_tool_response_item517–553 ↗
async fn handle_non_tool_response_item(
    sess: &Session,
    turn_context: &TurnContext,
    contributor_policy: TurnItemContributorPolicy<'_>,
    item: &ResponseItem,
    plan_mode: bool,
) -> Op

作用:把不是工具调用的模型输出,转成系统内部用于展示和记录的 TurnItem。它也会忽略那些不该直接从模型流里出现的工具输出。

数据流:进去的是会话、上下文、插件策略、输出项和计划模式 → 如果是消息、推理、网页搜索或图片生成,就先 parse_turn_item 转成 TurnItem,再调用 finalize_turn_item 做清理和补充;图片项还会记录图片保存说明 → 出来是可选 TurnItem。

调用关系:finalize_non_tool_response_item 会调用它完成主要转换;try_run_sampling_request 也会直接使用它;它把最后清理交给 finalize_turn_item,把图片提示交给 record_image_generation_instructions。

调用图:调用 2 个内部函数(finalize_turn_item, record_image_generation_instructions);被 2 处调用(try_run_sampling_request, finalize_non_tool_response_item);外部调用 2 个(parse_turn_item, debug!)。

finalize_turn_item555–586 ↗
async fn finalize_turn_item(
    sess: &Session,
    turn_context: &TurnContext,
    contributor_policy: TurnItemContributorPolicy<'_>,
    turn_item: &mut TurnItem,
    plan_mode: bool,
)

作用:对一个 TurnItem 做最后整理,让它适合展示、保存和后续判断。它会运行扩展补充、清理助手隐藏标记、解析记忆引用,并保存已完成的图片。

数据流:进去的是会话、上下文、插件策略、可修改 TurnItem 和计划模式 → 如果策略要求,就先运行 contributors;如果是助手消息,就合并文字、去掉隐藏标记、写入记忆引用;如果是已完成图片,就保存图片文件 → 出来没有新对象,但传入的 TurnItem 会被改成最终形态。

调用关系:handle_non_tool_response_item 和 emit_completed 会调用它;它内部根据情况调用 apply_turn_item_contributors、strip_hidden_assistant_markup_and_parse_memory_citation 和 persist_image_generation_item。

调用图:调用 3 个内部函数(apply_turn_item_contributors, persist_image_generation_item, strip_hidden_assistant_markup_and_parse_memory_citation);被 2 处调用(handle_non_tool_response_item, emit_completed);外部调用 1 个(vec!)。

last_assistant_message_from_item588–603 ↗
fn last_assistant_message_from_item(
    item: &ResponseItem,
    plan_mode: bool,
) -> Option<String>

作用:从一个输出项里取出“可以算作助手最后回复”的文本。空文本或只剩隐藏标记的内容不会被当成有效回复。

数据流:进去的是 ResponseItem 和计划模式标志 → 先提取助手原始文字;如果为空就返回 None;再清理隐藏标记;清理后仍为空也返回 None → 否则出来干净的助手文本。

调用关系:get_last_assistant_message_from_turn 会用它找最后助手消息;completed_item_defers_mailbox_delivery_to_next_turn 用它判断是否需要把邮箱投递推迟到下一轮;它依赖 raw_assistant_output_text_from_item 和 strip_hidden_assistant_markup。

调用图:调用 2 个内部函数(raw_assistant_output_text_from_item, strip_hidden_assistant_markup);被 2 处调用(get_last_assistant_message_from_turn, completed_item_defers_mailbox_delivery_to_next_turn)。

completed_item_defers_mailbox_delivery_to_next_turn605–621 ↗
fn completed_item_defers_mailbox_delivery_to_next_turn(
    item: &ResponseItem,
    plan_mode: bool,
) -> bool

作用:判断一条完成的输出是否应该让邮箱消息等到下一轮再送进来。这样正式回答或图片生成完成时,不会被当前轮新消息打断。

数据流:进去的是 ResponseItem 和计划模式 → 如果是助手的非 commentary(旁白/评论阶段)消息,就看是否有有效最后回复;如果是图片生成调用,直接认为要推迟;其他类型不推迟 → 出来是布尔值。

调用关系:record_completed_response_item_with_finalized_facts 在没有现成事实时会用同样的判断结果来控制 input_queue;它又调用 last_assistant_message_from_item 判断文字是否真的存在。

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

response_input_to_response_item623–664 ↗
fn response_input_to_response_item(input: &ResponseInputItem) -> Option<ResponseItem>

作用:把准备发回模型的输入项,转换成可以写进历史的输出项。主要用于把工具执行结果或工具拒绝消息也记录进对话账本。

数据流:进去的是 ResponseInputItem → 如果它是函数调用输出、自定义工具输出、MCP 工具输出或工具搜索输出,就复制里面的 call_id、名称、状态和结果,包装成对应的 ResponseItem → 其他输入类型返回 None。

调用关系:handle_output_item_done 在需要直接回复模型的工具错误路径里调用它;转换成功后,结果会通过 record_conversation_items 写入会话历史,让模型看到的工具回复和系统保存的历史保持一致。

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

core/src/tasks/lifecycle.rs源码 ↗
orchestrationturn lifecycle / thread idle

可以把这个文件理解成会话里的“广播员”。Session 是一场会话的核心对象,扩展插件可能想在某些关键时刻插一脚,比如回合刚开始时准备数据,回合结束时保存结果,出错时记录原因。这里的几个函数不直接处理用户问题,而是在合适的时间,把事件和相关资料打包成输入,逐个交给已注册的扩展。它还会把三层数据仓库一起传过去:会话级、线程级、当前回合级。这样扩展既能看长期信息,也能看本轮信息。比较重要的一点是,线程空闲通知不是随便发的,它会先检查当前有没有正在运行的回合,或者队列里有没有马上要触发新回合的消息;只有真的没活干时才通知扩展“现在空了”。

函数细节5
Session::emit_turn_start_lifecycle10–27 ↗
async fn emit_turn_start_lifecycle(
        &self,
        turn_context: &TurnContext,
        token_usage_at_turn_start: &TokenUsage,
    )

作用:在一个对话回合刚开始时,通知所有关心“回合开始”的扩展。扩展可以借这个机会初始化本轮需要的数据,或者记录开始时的状态。

数据流:进去的是当前回合的上下文和回合开始时的 token 用量;token 可以简单理解成模型读写文字时消耗的计量单位。函数从 Session 里取出所有回合生命周期扩展,把回合编号、协作模式、token 用量,以及会话级、线程级、回合级数据仓库打包起来,逐个异步交给扩展。出来没有普通返回值,但扩展可能会在自己的数据仓库里留下记录或准备好的状态。

调用关系:它处在“新回合启动”的通知环节。Session 的回合流程在真正开始处理一轮任务前会用它来广播消息;它自己不做具体业务,而是把工作交给每个扩展的 on_turn_start 回调。

Session::emit_turn_stop_lifecycle29–39 ↗
async fn emit_turn_stop_lifecycle(&self, turn_store: &ExtensionData)

作用:在一个对话回合正常停止时,通知所有关心“回合结束”的扩展。扩展可以用它来保存结果、做统计,或者清理本轮临时数据。

数据流:进去的是当前回合的数据仓库 turn_store。函数从 Session 中拿到会话级和线程级数据仓库,再连同这个回合级数据仓库一起打包,逐个交给所有回合生命周期扩展的停止回调。出来没有普通返回值,但扩展可以据此更新自己的保存内容或完成收尾。

调用关系:它位于一轮任务正常收尾的时候。Session 的流程会在回合结束点调用它;它再把通知传给每个扩展的 on_turn_stop,让扩展完成自己的结束动作。

Session::emit_thread_idle_lifecycle_if_idle41–56 ↗
async fn emit_thread_idle_lifecycle_if_idle(&self)

作用:在线程可能空闲时先做检查,确认真的没任务后,才通知扩展“现在空闲了”。这样可以避免系统刚要处理下一件事,却误报为空闲。

数据流:它不需要外部传入参数。函数会读取 active_turn,也就是当前是否有正在进行的回合;还会问输入队列里有没有会触发新回合的消息。如果任一条件表示还有活,它就直接返回。只有两边都显示没活干时,它才把会话级和线程级数据仓库打包,通知所有线程生命周期扩展。结果没有普通返回值,但扩展可能会趁空闲做后台整理或保存。

调用关系:它处在“看看线程是不是闲下来了”的关口,通常会被 Session 在任务处理间隙或回合结束后使用。它先自己把关,避免误通知;确认空闲后,再把事情交给每个扩展的 on_thread_idle。

Session::emit_turn_abort_lifecycle58–73 ↗
async fn emit_turn_abort_lifecycle(
        &self,
        reason: TurnAbortReason,
        turn_store: &ExtensionData,
    )

作用:在一个对话回合被中途取消或打断时,通知扩展发生了什么原因。扩展可以用它来记录取消原因,或者把未完成的临时状态清掉。

数据流:进去的是取消原因 reason 和当前回合的数据仓库 turn_store。函数会遍历所有回合生命周期扩展,把取消原因、会话级数据、线程级数据和回合级数据打包给它们。因为同一个原因要发给多个扩展,所以它会复制一份 reason 给每个扩展使用。出来没有普通返回值,但扩展可能会记录这次中断或做清理。

调用关系:它位于“回合异常中止”的通知环节。Session 在判断一轮任务不是正常结束而是被取消时会使用它;函数内部除了复制取消原因外,主要就是把通知交给每个扩展的 on_turn_abort。

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

Session::emit_turn_error_lifecycle75–91 ↗
async fn emit_turn_error_lifecycle(
        &self,
        turn_context: &TurnContext,
        error: CodexErrorInfo,
    )

作用:在一个对话回合遇到错误时,通知扩展具体错误信息。扩展可以用它来写错误日志、上报监控,或者保存方便排查的问题现场。

数据流:进去的是当前回合上下文和错误信息 error。函数从回合上下文里拿到回合编号和回合级数据仓库,从 Session 里拿到会话级和线程级数据仓库,再把这些连同错误一起交给所有回合生命周期扩展。因为要通知多个扩展,它会为每个扩展复制一份错误信息。出来没有普通返回值,但扩展可能会保存错误记录或触发自己的告警。

调用关系:它处在“回合出错”的通知环节。Session 的错误处理流程会调用它;它不修复错误,而是把错误现场交给每个扩展的 on_turn_error,让扩展做记录、诊断或后续处理。

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

core/src/tools/lifecycle.rs源码 ↗
orchestrationrequest handling / cross-cutting

系统里运行工具时,不只是工具本身要干活,旁边的扩展也可能需要知道发生了什么。比如一个扩展想统计工具耗时,或者在工具结束后保存一些信息,就需要收到“开始了”和“结束了”的通知。这个文件做的就是这件事:从一次工具调用里拿到会话、当前轮次、工具名、调用编号、来源等信息,然后逐个通知所有关心工具生命周期的扩展。“生命周期”就是一件事从开始到结束的过程。这里还会把项目内部使用的工具来源格式,转换成扩展接口能看懂的格式。这样核心系统和扩展之间不会互相乱读对方的内部结构,边界更清楚,也更不容易出错。

函数细节5
notify_tool_start12–31 ↗
async fn notify_tool_start(invocation: &ToolInvocation)

作用:这个函数在某个工具刚要开始运行时,通知所有订阅了工具生命周期的扩展。别人会用它来保证扩展能第一时间知道“哪个工具、在哪次对话里、以什么来源开始了”。

数据流:进去的是一次工具调用信息 invocation,里面带着会话、当前轮次、工具名、调用编号和调用来源。它从会话服务里取出所有生命周期扩展,把这些信息打包成 ToolStartInput,并把内部的来源类型转换成扩展能看懂的类型。出来没有普通返回值,但每个扩展都会收到一次“工具开始”的异步通知。

调用关系:它会在 dispatch_any_with_terminal_outcome 开始分发工具执行时被调用。它自己不执行工具,只负责把开工消息送出去;送消息前会调用 extension_tool_call_source,把内部来源翻译成扩展接口的来源。

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

notify_tool_finish33–43 ↗
async fn notify_tool_finish(invocation: &ToolInvocation, outcome: ToolCallOutcome)

作用:这个函数在工具正常走到结束通知阶段时使用,把工具最终结果告诉扩展。它是一个方便入口,调用者只要给它完整的 ToolInvocation 和结果即可。

数据流:进去的是工具调用 invocation 和一个 outcome,也就是工具这次的结局。它从 invocation 里拆出会话、轮次、调用编号、工具名和来源,然后交给 notify_tool_finish_parts 统一处理。出来没有普通返回值,但会触发扩展收到“工具结束”的通知。

调用关系:它会被 notify_tool_finish_if_unclaimed 调用,用在工具结果还没有被其他路径处理时。它本身像一个转接头,把完整调用对象拆成零件后交给 notify_tool_finish_parts,避免结束通知的代码写两遍。

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

notify_tool_aborted45–61 ↗
async fn notify_tool_aborted(
    session: &Session,
    turn: &TurnContext,
    call_id: &str,
    tool_name: &ToolName,
    source: ToolCallSource,
)

作用:这个函数用于工具被中止时通知扩展,比如运行过程被取消、没有正常完成。它会把结局固定标记为 Aborted,也就是“已中止”。

数据流:进去的是会话、当前轮次、调用编号、工具名和来源。它不会要求调用者再提供结果,而是直接把结果设为 ToolCallOutcome::Aborted,然后交给 notify_tool_finish_parts。出来没有普通返回值,但扩展会收到一次“工具结束,结果是中止”的通知。

调用关系:它和 notify_tool_finish 走同一套底层通知流程,都会调用 notify_tool_finish_parts。区别是它专门服务于中止场景,帮上层少传一个固定结果,也避免有人忘了把中止写成正确的结局。

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

notify_tool_finish_parts63–85 ↗
async fn notify_tool_finish_parts(
    session: &Session,
    turn: &TurnContext,
    call_id: &str,
    tool_name: &ToolName,
    source: ToolCallSource,
    outcome: ToolCallOutcome,
)

作用:这是发送“工具结束”通知的核心函数。它接收已经拆好的各项信息,然后逐个通知所有扩展这个工具最后是什么结果。

数据流:进去的是会话、轮次、调用编号、工具名、调用来源和工具结局。它从会话服务里拿到所有生命周期扩展,把会话级、线程级、轮次级的扩展数据仓库,以及工具身份和结果打包成 ToolFinishInput;同时把内部来源转换成扩展接口来源。出来没有普通返回值,但每个扩展都会依次收到 on_tool_finish 回调。

调用关系:notify_tool_finish 和 notify_tool_aborted 都会把活儿交给它,因为无论正常结束还是中止,通知扩展的过程大体一样。它在发送通知时会调用 extension_tool_call_source,并且会克隆来源信息,确保每个扩展拿到的来源内容都完整可用。

调用图:调用 1 个内部函数(extension_tool_call_source);被 2 处调用(notify_tool_aborted, notify_tool_finish);外部调用 1 个(clone)。

extension_tool_call_source87–98 ↗
fn extension_tool_call_source(source: ToolCallSource) -> ExtensionToolCallSource

作用:这个小函数负责把核心系统内部的“工具来源”说法,翻译成扩展 API 使用的“工具来源”说法。它的作用像翻译员,让内部代码和外部扩展不用共享同一个内部类型。

数据流:进去的是 ToolCallSource,也就是内部记录的工具从哪里被调用:可能是直接调用,也可能是代码模式里的某个单元格触发。它按来源种类一一对应转换;如果是代码模式,还会保留 cell_id 和 runtime_tool_call_id。出来的是 ExtensionToolCallSource,扩展接口可以直接使用。

调用关系:notify_tool_start 和 notify_tool_finish_parts 在通知扩展前都会调用它。这样开始通知和结束通知里的来源格式保持一致,扩展收到的数据也不会混进核心系统自己的内部类型。

调用图:被 2 处调用(notify_tool_finish_parts, notify_tool_start)。

core/src/user_shell_command.rs源码 ↗
domain_logicrequest handling / after shell command execution

当用户让系统记录一条 shell 命令的结果时,原始输出通常不能直接塞进对话上下文里:它可能很长,也需要带上退出码和耗时这些关键信息。这个文件就负责把这些零散信息包装成统一的“用户 shell 命令片段”。它先用当前回合的截断规则,把命令输出整理成合适长度;再把命令文本、退出码、耗时和整理后的输出合在一起。测试时,它可以把这段记录直接渲染成字符串,方便检查长什么样。正常运行时,它会把这段记录转换成 ResponseItem,也就是协议里能传递和保存的一条响应内容。这样后面的持久化或上下文构建就不用关心命令输出的细节,只接收标准格式。

函数细节3
user_shell_command_fragment9–16 ↗
fn user_shell_command_fragment(
    command: &str,
    exec_output: &ExecToolCallOutput,
    turn_context: &TurnContext,
) -> UserShellCommand

作用:把一次 shell 命令的原始执行结果,做成一个统一的命令记录片段。别人用它,是为了不用每次都自己拼命令、退出码、耗时和输出文本。

数据流:进去的是命令字符串、执行结果对象,以及当前回合的上下文。它先读取上下文里的截断策略,把执行输出整理成适合放进记录的文本;然后连同退出码和耗时一起交给 UserShellCommand::new,得到一个 UserShellCommand。出来的是一份结构化的命令记录,原始执行结果本身不会被改动。

调用关系:这是这个文件里的基础小零件。测试用的 format_user_shell_command_record 会先调用它再转成文字;正式流程里的 user_shell_command_record_item 也会先调用它,再把结果包装成 ResponseItem。它把具体格式化输出的活儿交给 format_exec_output_str,把创建记录对象的活儿交给 new。

调用图:调用 2 个内部函数(new, format_exec_output_str);被 2 处调用(format_user_shell_command_record, user_shell_command_record_item)。

format_user_shell_command_record19–25 ↗
fn format_user_shell_command_record(
    command: &str,
    exec_output: &ExecToolCallOutput,
    turn_context: &TurnContext,
) -> String

作用:这是测试用的辅助函数,把一条 shell 命令记录直接变成可读字符串。这样测试可以检查最终展示出来的文字是否符合预期。

数据流:进去的是命令、执行结果和当前回合上下文。它先调用 user_shell_command_fragment 做出结构化记录,再调用记录自己的 render 方法,把它变成字符串。出来的是一段文本;它不负责保存,也不改变外部状态。

调用关系:它只在测试配置下编译使用,主要服务于 user_shell_command_tests.rs 里的检查。它不参与正式运行流程,只是借用 user_shell_command_fragment,确保测试看到的内容和真实记录生成逻辑一致。

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

user_shell_command_record_item27–37 ↗
fn user_shell_command_record_item(
    command: &str,
    exec_output: &ExecToolCallOutput,
    turn_context: &TurnContext,
) -> ResponseItem

作用:把一次 shell 命令记录转换成协议层能保存或传递的 ResponseItem。简单说,它把内部记录装进系统对外统一使用的“消息盒子”里。

数据流:进去的是命令文本、执行结果和当前回合上下文。它先调用 user_shell_command_fragment 生成命令记录片段,然后通过 ContextualUserFragment::into 把这个片段转换成 ResponseItem。出来的是一条标准响应项,供后续流程持久化或放进上下文。

调用关系:这是正式流程会调用的出口函数,persist_user_shell_output 会在需要保存用户 shell 输出时使用它。它自己不直接写磁盘,也不直接处理协议细节,而是先复用 user_shell_command_fragment 生成内容,再把包装转换的工作交给 into。

调用图:调用 2 个内部函数(into, user_shell_command_fragment);被 1 处调用(persist_user_shell_output)。

tools/src/tool_output.rs源码 ↗
domain_logicrequest handling / tool execution

系统里有很多工具,工具的返回结果不一定长得一样。这个文件就是给这些结果定规矩:怎样生成给模型看的内容,怎样生成给日志看的简短预览,怎样告诉日志这次算不算成功,怎样把结果交给工具使用后的钩子(hook,某个动作结束后自动触发的小程序)。它的核心是 ToolOutput 这个 trait,可以理解成“工具输出必须会做的几件事”。JsonToolOutput 是最常见的一种输出,把 JSON 值包起来,还能标记成功失败、是否带有外部上下文。文件还支持 MCP 工具结果,MCP 可以理解成一种让外部工具接入系统的协议。另一个重要点是 telemetry_preview:日志不能随便塞进超长内容,所以它会按字节和行数截断预览,避免日志爆炸,同时不切坏中文这类多字节字符。

函数细节29
ToolOutput::contains_external_context23–25 ↗
fn contains_external_context(&self) -> bool

作用:告诉系统这个工具结果里有没有“外部上下文”,也就是来自系统外部、可能影响记忆生成的信息。默认回答是没有。

数据流:进去的是某个工具输出对象本身 → 默认不检查任何内容,直接认为它不含外部上下文 → 出来一个 false,不改动任何东西。

调用关系:这是 ToolOutput 这套统一接口的一部分。具体输出类型可以覆盖它,比如 JsonToolOutput 就会根据自己的标记返回真实情况。

ToolOutput::post_tool_use_id30–32 ↗
fn post_tool_use_id(&self, call_id: &str) -> String

作用:决定工具使用后钩子看到的工具调用编号。默认就是原来的 call_id。

数据流:进去一个 call_id 字符串 → 默认复制一份 → 出来给 PostToolUse 钩子使用的编号,不改动原对象。

调用关系:post_unified_exec_tool_use_payload 和 post_tool_use_payload 在准备钩子数据时会调用它。多数输出不需要特殊编号,所以用默认行为即可。

调用图:被 2 处调用(post_unified_exec_tool_use_payload, post_tool_use_payload)。

ToolOutput::post_tool_use_input35–37 ↗
fn post_tool_use_input(&self, _payload: &ToolPayload) -> Option<JsonValue>

作用:决定工具使用后钩子能不能看到这次工具的输入。默认不给输入。

数据流:进去工具输出对象和工具 payload(工具调用携带的信息)→ 默认忽略 payload → 出来 None,表示没有给钩子的输入数据。

调用关系:post_tool_use_payload 和 post_unified_exec_tool_use_payload 会问它要输入数据。某些特殊工具输出可以覆盖它,把稳定的输入格式交给钩子。

调用图:被 3 处调用(post_tool_use_payload, post_unified_exec_tool_use_payload, post_tool_use_payload)。

ToolOutput::post_tool_use_response46–48 ↗
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue>

作用:决定工具使用后钩子能不能看到这次工具的输出。默认不给输出,这表示“不参与钩子输出”,不等于“输出为空”。

数据流:进去 call_id、payload 和输出对象 → 默认都不使用 → 出来 None,不产生钩子响应数据。

调用关系:多个 post_tool_use_payload 流程和 post_unified_exec_tool_use_payload 会调用它。JsonToolOutput 会覆盖它,把 JSON 结果交给钩子。

调用图:被 5 处调用(post_tool_use_payload, post_tool_use_payload, post_tool_use_payload, post_unified_exec_tool_use_payload, post_tool_use_payload)。

ToolOutput::code_mode_result50–52 ↗
fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue

作用:把工具结果转成“代码模式”更好处理的 JSON 值。默认做法是先转成模型回复项,再从回复项里提取 JSON 表示。

数据流:进去工具输出对象和 payload → 调用 to_response_item 生成统一的 ResponseInputItem → 交给 response_input_to_code_mode_result 转换 → 出来一个 serde_json::Value,不直接改动原输出。

调用关系:tool_dispatch_result 会在整理工具执行结果时调用它。它把具体输出类型和代码模式之间隔开,让上层不用关心每种工具输出的细节。

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

Box::log_preview59–61 ↗
fn log_preview(&self) -> String

作用:让装在 Box 里的工具输出也能正常生成日志预览。Box 可以理解成“把对象放进盒子里,通过指针使用”。

数据流:进去一个 Box 包着的 ToolOutput → 打开盒子,把请求转交给里面真正的对象 → 出来里面对象生成的预览字符串。

调用关系:这是给 Box<T> 实现 ToolOutput 的转发函数。它不做业务判断,只是保证被装箱的输出还能像普通输出一样使用。

Box::success_for_logging63–65 ↗
fn success_for_logging(&self) -> bool

作用:让装在 Box 里的工具输出也能报告日志里的成功或失败状态。

数据流:进去一个 Box 包着的输出 → 转交给盒子里的真实输出 → 出来 true 或 false。

调用关系:它是 Box 的接口转发层。上层即使拿到的是 Box<dyn ToolOutput>,也能统一询问成功状态。

Box::contains_external_context67–69 ↗
fn contains_external_context(&self) -> bool

作用:让装在 Box 里的工具输出也能说明自己是否包含外部上下文。

数据流:进去一个 Box 输出 → 调用里面对象的 contains_external_context → 出来里面对象给出的布尔值。

调用关系:这是 Box 对 ToolOutput 的透明转发。真正判断逻辑在被包装的输出类型里。

Box::to_response_item71–73 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem

作用:让装在 Box 里的工具输出也能转成模型能接收的统一回复项。

数据流:进去 Box 输出、call_id 和 payload → 把这些参数转交给里面对象的 to_response_item → 出来 ResponseInputItem。

调用关系:这是为了让动态包装后的工具输出仍能进入模型回复流程。它本身不决定格式,只负责转发。

Box::post_tool_use_id75–77 ↗
fn post_tool_use_id(&self, call_id: &str) -> String

作用:让装在 Box 里的工具输出也能决定钩子看到的调用编号。

数据流:进去 Box 输出和 call_id → 转交给里面对象 → 出来钩子使用的 id 字符串。

调用关系:post_tool_use 相关流程如果拿到的是装箱输出,也可以通过这个转发拿到正确 id。

Box::post_tool_use_input79–81 ↗
fn post_tool_use_input(&self, payload: &ToolPayload) -> Option<JsonValue>

作用:让装在 Box 里的工具输出也能提供工具使用后钩子需要的输入数据。

数据流:进去 Box 输出和 payload → 转交给里面对象 → 出来 Option<JsonValue>,有值就给钩子,没有就不提供。

调用关系:它让 Box 不会挡住具体输出类型的钩子能力。真正要不要提供输入,由里面的对象决定。

Box::post_tool_use_response83–85 ↗
fn post_tool_use_response(&self, call_id: &str, payload: &ToolPayload) -> Option<JsonValue>

作用:让装在 Box 里的工具输出也能提供工具使用后钩子需要的输出数据。

数据流:进去 Box 输出、call_id 和 payload → 转交给里面对象 → 出来可选的 JSON 响应。

调用关系:这是钩子流程里的透明转发层。它保证装箱不会改变输出类型原本的行为。

Box::code_mode_result87–89 ↗
fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue

作用:让装在 Box 里的工具输出也能生成代码模式使用的 JSON 结果。

数据流:进去 Box 输出和 payload → 调用里面对象的 code_mode_result → 出来 JSON 值。

调用关系:tool_dispatch_result 等上层流程不需要管输出有没有被 Box 包住,照样能拿到代码模式结果。

JsonToolOutput::new100–106 ↗
fn new(value: JsonValue) -> Self

作用:创建一个最普通的 JSON 工具输出,并默认认为这次工具执行成功。

数据流:进去一个 JsonValue → 包进 JsonToolOutput,success 设为 Some(true),外部上下文标记设为 false → 出来一个新的 JsonToolOutput。

调用关系:handle_call、handle、goal_response 和一些测试会用它快速包装工具结果。它是 JSON 工具输出最常见的入口。

调用图:被 13 处调用(handle_call, handle_call, handle, exposes_generic_hook_payloads, post_tool_use_feedback_output_keeps_code_mode_result_typed, handle_call, handle, goal_response, handle_call, handle_call (+3 more))。

JsonToolOutput::with_success108–114 ↗
fn with_success(value: JsonValue, success: Option<bool>) -> Self

作用:创建一个 JSON 工具输出,同时明确指定成功、失败,或者不说明成功状态。

数据流:进去一个 JsonValue 和 success 选项 → 把值和状态一起放进 JsonToolOutput,外部上下文标记默认 false → 出来新的 JsonToolOutput。

调用关系:当工具结果不只是“有 JSON 就算成功”时会用它,比如需要把失败状态也传给模型或日志。

JsonToolOutput::with_external_context116–119 ↗
fn with_external_context(mut self) -> Self

作用:把一个 JSON 输出标记为包含外部上下文。这个标记可用于后续决定是否禁用记忆生成。

数据流:进去一个已有的 JsonToolOutput → 把 contains_external_context 改成 true → 出来修改后的同一个输出对象。

调用关系:它通常接在 JsonToolOutput::new 或 with_success 后面用。后续 contains_external_context 会读到这个标记。

JsonToolOutput::log_preview123–125 ↗
fn log_preview(&self) -> String

作用:为 JSON 工具结果生成一段适合写进日志的短预览,避免日志被超长结果撑爆。

数据流:进去 JsonToolOutput → 把内部 JSON 转成字符串 → 交给 telemetry_preview 截短 → 出来预览文本。

调用关系:日志系统需要展示工具输出时会通过 ToolOutput 接口调用它。它把具体截断规则交给 telemetry_preview。

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

JsonToolOutput::success_for_logging127–129 ↗
fn success_for_logging(&self) -> bool

作用:告诉日志这次 JSON 工具输出算不算成功。没明确写成功状态时,默认按成功处理。

数据流:进去 JsonToolOutput → 读取 success 字段 → 如果是 None 就当 true → 出来布尔值。

调用关系:这是 ToolOutput 的具体实现。日志记录工具执行状态时会用这个答案。

JsonToolOutput::contains_external_context131–133 ↗
fn contains_external_context(&self) -> bool

作用:返回这个 JSON 输出是否被标记为含有外部上下文。

数据流:进去 JsonToolOutput → 读取 contains_external_context 字段 → 出来 true 或 false。

调用关系:它覆盖了 ToolOutput 的默认 false。with_external_context 设置的标记会在这里被读出来。

JsonToolOutput::to_response_item135–153 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem

作用:把 JSON 工具输出包装成模型能接收的回复项,并根据工具类型选择普通函数输出还是自定义工具输出。

数据流:进去 JsonToolOutput、call_id 和 ToolPayload → 把 JSON 转成文本形式,连同 success 放进 FunctionCallOutputPayload → 如果 payload 是 Custom 就生成 CustomToolCallOutput,否则生成 FunctionCallOutput → 出来 ResponseInputItem。

调用关系:这是 JSON 输出进入模型上下文的关键转换点。ToolOutput::code_mode_result 默认也会依赖类似的统一回复项。

调用图:外部调用 3 个(to_string, matches!, Text)。

JsonToolOutput::post_tool_use_response155–157 ↗
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue>

作用:把 JSON 工具结果原样提供给工具使用后的钩子。

数据流:进去 JsonToolOutput,以及未使用的 call_id、payload → 克隆内部 JSON 值 → 出来 Some(JsonValue)。

调用关系:post_tool_use 相关流程会调用 ToolOutput::post_tool_use_response。JsonToolOutput 通过这个实现让钩子拿到结构化结果,而不是只能拿到字符串。

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

JsonToolOutput::code_mode_result159–161 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue

作用:给代码模式直接返回原始 JSON,而不是把它变成字符串后再解析。

数据流:进去 JsonToolOutput 和未使用的 payload → 克隆内部 JSON 值 → 出来这个 JSON 值本身。

调用关系:它覆盖了 ToolOutput 的默认转换逻辑,避免 JSON 输出在代码模式里丢掉类型信息,比如数字仍然是数字、对象仍然是对象。

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

CallToolResult::log_preview165–169 ↗
fn log_preview(&self) -> String

作用:为 MCP 工具结果生成日志预览。MCP 是外部工具接入的一种协议,所以它的结果格式和普通 JSON 输出不一样。

数据流:进去一个 CallToolResult → 先转成函数调用输出格式 → 优先拿正文里的文本,如果没有文本就把整个输出转成字符串 → 交给 telemetry_preview 截短 → 出来日志预览。

调用关系:这是给 MCP 结果实现 ToolOutput 的一部分。日志系统通过统一接口调用它,不需要知道 MCP 内部格式。

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

CallToolResult::success_for_logging171–173 ↗
fn success_for_logging(&self) -> bool

作用:告诉日志这次 MCP 工具调用是否成功。

数据流:进去 CallToolResult → 调用它自己的 success 判断 → 出来 true 或 false。

调用关系:这是 MCP 输出接入统一 ToolOutput 接口的成功状态部分。日志记录时会用到它。

CallToolResult::to_response_item175–180 ↗
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem

作用:把 MCP 工具结果包装成模型输入项,让模型能看到外部工具返回了什么。

数据流:进去 CallToolResult、call_id 和未使用的 payload → 克隆 MCP 结果并带上调用编号 → 出来 ResponseInputItem::McpToolCallOutput。

调用关系:工具执行完成后,上层会通过 ToolOutput 把结果送回模型。这个函数负责 MCP 这一路的格式转换。

CallToolResult::code_mode_result182–186 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue

作用:把 MCP 工具结果转成代码模式可用的 JSON。如果序列化失败,就返回一条说明失败原因的字符串。

数据流:进去 CallToolResult → 尝试用 serde_json 转成 JsonValue → 成功就输出结构化 JSON,失败就输出包含错误信息的字符串。

调用关系:它覆盖默认 code_mode_result,因为 MCP 结果本身适合直接序列化。代码模式拿到结果时不需要经过普通回复项再转换。

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

response_input_to_code_mode_result189–221 ↗
fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue

作用:把各种模型回复项统一压成代码模式要用的 JSON 值。它像一个分拣员:不同类型的回复走不同转换规则。

数据流:进去一个 ResponseInputItem → 如果是普通消息,就把文本和图片地址提取出来;如果是函数输出文本,就变成 JSON 字符串;如果是内容列表,就交给 content_items_to_code_mode_result;如果是工具搜索结果,就变成数组;如果是 MCP 输出,就尝试序列化 → 出来一个 JsonValue。

调用关系:ToolOutput::code_mode_result 的默认实现会调用它。它把“模型回复格式”转换成“代码模式结果格式”,是两种表示之间的桥。

调用图:调用 1 个内部函数(content_items_to_code_mode_result);被 1 处调用(code_mode_result);外部调用 3 个(Array, String, to_value)。

content_items_to_code_mode_result223–243 ↗
fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue

作用:从一组内容项里提取人能读、代码模式也能用的文本,把空文本和加密内容跳过。

数据流:进去一组 FunctionCallOutputContentItem → 遍历每一项,保留非空文本和非空图片地址,忽略空内容和加密内容 → 用换行拼成一个字符串 → 出来 JsonValue::String。

调用关系:response_input_to_code_mode_result 在遇到消息内容列表或函数输出内容列表时会调用它。它专门负责把多段内容合成一个简单结果。

调用图:被 1 处调用(response_input_to_code_mode_result);外部调用 2 个(String, iter)。

telemetry_preview245–283 ↗
fn telemetry_preview(content: &str) -> String

作用:生成安全的遥测日志预览。遥测就是系统运行时收集的诊断信息;这里要防止工具输出太大,把日志撑爆。

数据流:进去一整段文本 → 先按最多 2048 字节截断,并确保不从中文等字符中间切开 → 再最多保留 64 行 → 如果确实截断了,就补上一行“预览已截断”的提示 → 出来适合写日志的字符串。

调用关系:JsonToolOutput::log_preview 和 CallToolResult::log_preview 都会调用它。它把所有工具输出的日志截断规则集中在一个地方,保证行为一致。

调用图:被 2 处调用(log_preview, log_preview);外部调用 2 个(new, take_bytes_at_char_boundary)。

机器可读与辅助输出

这些文件将通知适配为 exec JSONL 输出,以及主线程 UI 之外的其他专用进度渲染或报告界面。

cli/src/doctor/output.rs源码 ↗
io_transportdoctor 命令完成检查后、把报告输出到终端或生成脱敏展示内容时活跃

可以把这个文件理解成“体检报告排版员”。前面的代码负责检查系统、配置、网络等是否正常;这里不决定检查什么,只决定怎么把结果展示给人看。它会把检查项按环境、配置、更新、联网、后台服务分组,给每行加上对勾、警告、叉号等标记,并根据用户选项决定是否显示详细信息、是否用纯 ASCII 字符、是否上色。它还会把一些值得注意但不一定算失败的情况提到顶部 Notes 区,比如有新版本、沙箱不够严格、登录方式信号混杂。另一个重要点是脱敏:详细信息里可能有 API key、token、URL 用户名密码等,这里会在输出前尽量改成 <redacted> 或裁掉敏感路径。文件底部有大量测试,确保不同显示模式、颜色、摘要、脱敏规则都不会悄悄变坏。

函数细节88
render_human_report73–117 ↗
fn render_human_report(report: &DoctorReport, options: HumanOutputOptions) -> String

作用:把完整的医生检查报告排成一整段给人看的终端文字。用户运行 codex doctor 时,最后看到的主要内容就是它拼出来的。

数据流:输入是一份 DoctorReport 报告和显示选项,比如是否显示细节、是否用颜色、是否只用 ASCII 字符。它先写标题和顶部提示,再按固定分组挑出检查项,逐行写入结果,最后加分隔线、总计和使用提示;输出是一整个字符串,不直接改报告本身。

调用关系:这是本文件的总入口。它会请 notes_for_report 找出顶部提醒,请 checks_for_group 按分组取检查项,请 write_note_rowwrite_check_row 写每一行,最后交给 write_footer 写底部提示;测试大量调用它来确认各种显示效果。

调用图:调用 5 个内部函数(checks_for_group, notes_for_report, write_check_row, write_footer, write_note_row);被 10 处调用(render_human_report_can_emit_color, render_human_report_expands_feature_flags_with_all, render_human_report_explains_terminal_warning_issue, render_human_report_includes_details_by_default_without_color, render_human_report_includes_memories_db_in_state_health_summary, render_human_report_includes_redacted_details, render_human_report_includes_threads_row_in_environment, render_human_report_promotes_notes_without_changing_statuses, render_human_report_supports_ascii_output, render_human_report_supports_summary_output_without_color);外部调用 2 个(new, writeln!)。

checks_for_group119–130 ↗
fn checks_for_group(report: &'a DoctorReport, group: &OutputGroup) -> Vec<&'a DoctorCheck>

作用:从总报告里挑出属于某个显示分组的检查项。这样终端报告能按“环境、配置、联网”等主题排好,而不是杂乱堆在一起。

数据流:输入是报告和一个分组定义,分组里有一组类别名。它遍历报告里的检查项,只留下类别匹配这些类别名的项;输出是这些检查项的引用列表,不改动原报告。

调用关系render_human_report 在写每个分组标题后调用它,拿到这一组该显示的行。

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

write_check_row132–148 ↗
fn write_check_row(out: &mut String, check: &DoctorCheck, options: HumanOutputOptions)

作用:写一条普通检查结果行,比如“git 正常”或“auth 失败”。如果用户要看细节,它还会把这项下面的详细说明也写出来。

数据流:输入是正在累积的输出字符串、一个检查项和显示选项。它先决定这一项的状态标记和一句摘要,再写成一行;如果开启细节,就从 detail_lines 取出可显示的细节并逐条写入。结果是输出字符串被追加内容。

调用关系render_human_report 对每个检查项调用它。它内部把摘要判断交给 row_description,把状态判断交给 display_status,把细节行交给 write_detail_line

调用图:调用 4 个内部函数(detail_lines, display_status, row_description, write_detail_line);被 1 处调用(render_human_report);外部调用 1 个(writeln!)。

write_note_row150–158 ↗
fn write_note_row(out: &mut String, note: &DoctorNote, options: HumanOutputOptions)

作用:写顶部 Notes 区的一条提醒。Notes 用来把“值得马上看”的情况提前展示出来。

数据流:输入是输出字符串、一条提醒和显示选项。它把提醒的状态标记、名字和摘要拼成一行追加到输出里;返回时只改变字符串内容。

调用关系render_human_report 在报告开头有 Notes 时调用它,一条提醒写一次。

调用图:被 1 处调用(render_human_report);外部调用 1 个(writeln!)。

write_detail_line160–213 ↗
fn write_detail_line(out: &mut String, detail: HumanDetail, options: HumanOutputOptions)

作用:写检查项下面缩进的详细信息。它能区分普通键值行、续行、项目符号和修复建议。

数据流:输入是一条 HumanDetail 细节和显示选项。它根据细节类型选择不同的前缀、缩进和颜色,把标签、值、期望值或建议文字格式化后追加到输出字符串。

调用关系write_check_row 在详细模式下调用它。它会使用 detail_value 给细节值做更细的高亮。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(write_check_row);外部调用 2 个(format!, writeln!)。

row_description215–229 ↗
fn row_description(check: &DoctorCheck, options: HumanOutputOptions) -> String

作用:决定一条检查结果主行右侧该显示什么话。它会优先把真正的问题或修复建议说清楚,而不是只显示泛泛的摘要。

数据流:输入是检查项和显示选项。若检查失败或警告且有问题列表,就生成问题摘要;若有修复建议,就把摘要和建议连起来;否则使用对应类别的友好摘要。输出是一句字符串。

调用关系write_check_row 调用它来得到主行文字。它会进一步调用 issue_summarydisplay_summary

调用图:调用 2 个内部函数(display_summary, issue_summary);被 1 处调用(write_check_row);外部调用 2 个(format!, matches!)。

issue_summary231–246 ↗
fn issue_summary(check: &DoctorCheck) -> String

作用:把一个检查项里的问题列表压缩成一句好读的话。问题多时不会全部塞进主行,只展示数量和前几个原因。

数据流:输入是一个检查项。没有问题时返回原摘要,一个问题时返回这个问题的原因,多个问题时返回“几个问题 + 前两个原因”的短句。

调用关系row_descriptionactionable_note_summary 会调用它,用在主检查行和顶部提醒里。

调用图:被 2 处调用(actionable_note_summary, row_description);外部调用 1 个(format!)。

display_status264–280 ↗
fn display_status(check: &DoctorCheck) -> DisplayStatus

作用:把底层检查状态转换成终端显示用的状态。它额外把“后台服务正常但没运行”显示成空闲,而不是普通正常。

数据流:输入是检查项。它先特殊检查 app-server 是否是“not running”,如果是就返回 Idle;否则把 Ok、Warning、Fail 映射成显示状态。

调用关系write_check_row 用它决定行标记,StatusCounts::from_report 用它统计底部汇总。

调用图:被 2 处调用(from_report, write_check_row)。

status_marker282–308 ↗
fn status_marker(status: DisplayStatus, options: HumanOutputOptions) -> String

作用:根据状态生成对勾、警告、叉号等图标,并按需要加颜色。它让用户一眼扫出哪些地方正常、哪些有问题。

数据流:输入是显示状态和显示选项。它先根据是否 ASCII 选择符号,比如 `[ok]`,再按状态套上绿色、橙色、红色或灰色;输出是格式化后的标记字符串。

调用关系status_marker_slot 调用它来给每行前面准备固定标记。

调用图:调用 5 个内部函数(amber, dim, green, orange, red);被 1 处调用(status_marker_slot)。

status_marker_slot310–313 ↗
fn status_marker_slot(status: DisplayStatus, options: HumanOutputOptions) -> String

作用:给状态标记后面补一个空格,方便主行排版对齐。

数据流:输入是状态和显示选项。它调用 status_marker 得到图标,再在后面加空格;输出是可直接放在行首的字符串。

调用关系:检查行和提醒行都会用到这种行首槽位,让报告看起来整齐。

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

style_description315–326 ↗
fn style_description(
    description: &str,
    status: DisplayStatus,
    options: HumanOutputOptions,
) -> String

作用:给主行摘要做颜色和命令高亮。正常或空闲信息会变淡,更新和问题信息会更醒目。

数据流:输入是一句描述、状态和选项。它先高亮文字里的命令或参数,再按状态决定变灰、变黄或保持醒目;输出是样式化字符串。

调用关系style_note_summary 会调用它,检查行摘要也依赖同样的样式思路。

调用图:调用 3 个内部函数(amber, dim, highlight_actions);被 1 处调用(style_note_summary)。

detail_marker328–333 ↗
fn detail_marker(is_issue: bool, options: HumanOutputOptions) -> String

作用:给有问题的细节行生成一个小箭头标记。普通细节不显示明显标记,问题细节会突出。

数据流:输入是这行是否代表问题,以及显示选项。不是问题就返回空白;是问题就返回 ASCII 的 > 或 Unicode 的 ,并可染成橙色。

调用关系write_detail_line 在写带 expected 的细节行时用它。

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

style_note_summary335–340 ↗
fn style_note_summary(note: &DoctorNote, options: HumanOutputOptions) -> String

作用:给 Notes 区提醒摘要加样式。更新提醒有专门样式,其它提醒按普通状态样式处理。

数据流:输入是一条提醒和显示选项。若提醒是更新状态,就交给 style_update_note_summary;否则交给 style_description。输出是可打印的摘要字符串。

调用关系write_note_row 写提醒行时使用它。

调用图:调用 2 个内部函数(style_description, style_update_note_summary)。

style_update_note_summary342–363 ↗
fn style_update_note_summary(summary: &str, options: HumanOutputOptions) -> String

作用:专门美化“有新版本可用”的提醒,让新版本号醒目,括号里的当前版本等上下文变淡。

数据流:输入是更新摘要和显示选项。没开颜色就原样返回;开了颜色后,它尝试拆出“版本 available”和括号说明,分别上色或变暗;输出是样式化字符串。

调用关系style_note_summary 遇到更新提醒时调用它,测试也直接检查它的颜色行为。

调用图:调用 1 个内部函数(amber);被 2 处调用(style_note_summary, update_note_emphasizes_available_version_and_dims_context);外部调用 1 个(format!)。

summary_line365–404 ↗
fn summary_line(report: &DoctorReport, options: HumanOutputOptions) -> String

作用:生成报告底部那一行总计,比如多少项正常、多少提醒、多少警告、多少失败,以及整体状态。

数据流:输入是报告和显示选项。它重新计算 Notes 数量,统计各类状态,选择分隔符,拼出每种数量标签和整体状态标签;输出是一行字符串。

调用关系render_human_report 在所有分组写完后用它生成总结。它调用 StatusCounts::from_reportcount_labeloverall_status_label

调用图:调用 5 个内部函数(from_report, count_label, dim, notes_for_report, overall_status_label);外部调用 2 个(format!, vec!)。

count_label406–421 ↗
fn count_label(
    count: usize,
    label: &str,
    status: DisplayStatus,
    options: HumanOutputOptions,
) -> String

作用:把“数量 + 状态名”格式化成底部汇总里的一个小块,例如 12 ok1 fail

数据流:输入是数量、文字标签、状态和显示选项。它让数量变淡,再按状态给标签上色;输出是一段汇总文字。

调用关系summary_line 会为 ok、idle、notes、warn、fail 分别调用它。

调用图:调用 5 个内部函数(amber, dim, green, orange, red);被 1 处调用(summary_line);外部调用 1 个(format!)。

overall_status_label423–429 ↗
fn overall_status_label(status: CheckStatus) -> &'static str

作用:把整体检查状态换成更适合人看的单词。比如警告状态显示成 degraded,意思是“能用但有退化”。

数据流:输入是 CheckStatus。它把 Ok、Warning、Fail 分别映射到 okdegradedfailed;输出是固定字符串。

调用关系summary_line 调用它来写底部最后的整体结论。

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

styled_overall_status431–441 ↗
fn styled_overall_status(label: &str, status: CheckStatus, options: HumanOutputOptions) -> String

作用:给整体状态标签加粗和颜色,让最后的结论更醒目。

数据流:输入是状态文字、原始状态和显示选项。没开颜色就原样返回;开了颜色就按成功、警告、失败分别使用绿、黄、红并加粗。

调用关系:它服务于底部汇总行,用来突出报告的最终结论。

header_suffix480–490 ↗
fn header_suffix(report: &DoctorReport) -> String

作用:生成标题后面的版本和平台信息,例如 Codex 版本加运行平台。这样用户截图或复制报告时,别人能知道它运行在哪个环境。

数据流:输入是报告。它先拿 Codex 版本,再尝试从 runtime 检查项里读平台;有平台就拼成“版本 · 平台”,没有就只返回版本。

调用关系render_human_report 写标题时使用它。

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

notes_for_report492–516 ↗
fn notes_for_report(report: &DoctorReport) -> Vec<DoctorNote>

作用:从完整报告里提炼顶部 Notes。它把“虽然不一定失败,但用户应该先注意”的内容集中放到最前面。

数据流:输入是报告。它依次查更新、状态文件、沙箱、所有非正常检查、认证和联网信号,能生成提醒的就加入列表;输出是提醒列表。

调用关系render_human_report 用它写 Notes 区,summary_line 用它统计提醒数量。它把具体判断交给 update_noterollout_notesandbox_notenon_ok_notesauth_reachability_note

调用图:调用 6 个内部函数(auth_reachability_note, find_check, non_ok_notes, rollout_note, sandbox_note, update_note);被 2 处调用(render_human_report, summary_line);外部调用 1 个(new)。

find_check518–523 ↗
fn find_check(report: &'a DoctorReport, category: &str) -> Option<&'a DoctorCheck>

作用:按类别名从报告里找某一项检查。它是很多提醒规则的“查资料”小工具。

数据流:输入是报告和类别字符串。它遍历检查项,找到第一个类别相同的就返回;找不到就返回空。

调用关系notes_for_reportauth_reachability_note 用它定位 updates、state、sandbox、websocket、reachability 等检查项。

调用图:被 2 处调用(auth_reachability_note, notes_for_report)。

update_note525–545 ↗
fn update_note(check: &DoctorCheck, report: &DoctorReport) -> Option<DoctorNote>

作用:判断是否应该提醒用户有新版本。如果检测到新版,它会生成一条更新提醒。

数据流:输入是 updates 检查项和总报告。它读取“最新版本状态”“最新版本”“已忽略版本”等细节;只有状态表示有新版时才输出提醒,否则输出空。

调用关系notes_for_report 在找到 updates 检查项后调用它。它依赖 detail 模块读取细节和值是否为空假。

调用图:调用 2 个内部函数(detail_value, is_falsy);被 1 处调用(notes_for_report);外部调用 1 个(format!)。

rollout_note547–562 ↗
fn rollout_note(check: &DoctorCheck) -> Option<DoctorNote>

作用:判断本地 rollout 文件是否过多或占空间过大,并在需要时提醒。rollout 文件可以理解成程序留下的状态/实验记录。

数据流:输入是 state 检查项。它读取活跃 rollout 文件统计,解析出文件数和字节数;如果数量或体积超过阈值,就输出一条警告提醒。

调用关系notes_for_report 在 state 检查存在时调用它。

调用图:调用 2 个内部函数(detail_value, rollout_files_and_bytes);被 1 处调用(notes_for_report);外部调用 1 个(format!)。

sandbox_note564–575 ↗
fn sandbox_note(check: &DoctorCheck) -> Option<DoctorNote>

作用:提醒用户沙箱限制不够严格。沙箱就是给程序划边界,防止它随便读写文件或联网。

数据流:输入是 sandbox 检查项。它读取文件系统沙箱和网络沙箱状态;两者都 restricted 时不提醒,否则生成一条警告,说明当前限制情况。

调用关系notes_for_report 在 sandbox 检查存在时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(notes_for_report);外部调用 1 个(format!)。

non_ok_notes577–588 ↗
fn non_ok_notes(report: &DoctorReport) -> Vec<DoctorNote>

作用:把所有警告或失败的检查项提升成顶部提醒。这样用户不用翻完整报告,也能先看到问题项。

数据流:输入是报告。它筛出状态为 Warning 或 Fail 的检查项,为每项生成状态、名称和可行动摘要;输出提醒列表。

调用关系notes_for_report 调用它来补充普通问题提醒。

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

actionable_note_summary590–598 ↗
fn actionable_note_summary(check: &DoctorCheck) -> String

作用:给顶部提醒生成一条“能指导下一步”的摘要。它会优先显示具体问题,其次显示修复建议。

数据流:输入是检查项。有 issue 就用 issue_summary;没有 issue 但有 remediation,就把摘要和建议拼起来;都没有就用原摘要。

调用关系non_ok_notes 为每条非正常检查生成提醒时使用它。

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

auth_reachability_note600–615 ↗
fn auth_reachability_note(report: &DoctorReport) -> Option<DoctorNote>

作用:发现登录方式和联网检测方式互相打架时提醒用户。比如同时有 ChatGPT 登录和 API key 环境变量,HTTP 检测可能走了 API key。

数据流:输入是报告。它找 websocket 和 reachability 两项,读取 auth mode 和 reachability mode,转成小写比较;如果出现 ChatGPT 与 API key 混用信号,就输出一条警告提醒。

调用关系notes_for_report 调用它作为额外一致性检查,它自己通过 find_check 找需要的检查项。

调用图:调用 2 个内部函数(detail_value, find_check);被 1 处调用(notes_for_report)。

display_summary617–635 ↗
fn display_summary(check: &DoctorCheck, options: HumanOutputOptions) -> String

作用:为不同类别的检查生成更友好的主行摘要。它避免把原始、冗长或技术味太重的 summary 直接丢给用户。

数据流:输入是检查项和显示选项。它按类别分派给 system、runtime、git、sandbox、websocket 等专门摘要函数;不认识的类别就返回原摘要。

调用关系row_description 在没有更严重问题要展示时调用它。

调用图:调用 12 个内部函数(app_server_summary, git_summary, mcp_summary, network_summary, runtime_summary, sandbox_summary, search_summary, state_summary, system_summary, terminal_summary (+2 more));被 1 处调用(row_description)。

system_summary637–639 ↗
fn system_summary(check: &DoctorCheck) -> String

作用:给系统环境检查生成短摘要,优先显示操作系统语言。

数据流:输入是 system 检查项。它尝试读取 os language 细节;有就返回该值,没有就返回原摘要。

调用关系display_summary 处理 system 类别时调用它。

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

runtime_summary641–648 ↗
fn runtime_summary(check: &DoctorCheck) -> String

作用:给运行时来源生成短摘要,特别识别本地 debug 构建。debug 构建就是开发时编出来的版本,不是正常安装包。

数据流:输入是 runtime 检查项。它先看当前可执行文件路径是否包含 /target/debug/,是就返回“local debug build”;否则尝试返回安装方式,拿不到就用原摘要。

调用关系display_summary 处理 runtime 类别时调用它。

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

search_summary650–660 ↗
fn search_summary(check: &DoctorCheck) -> String

作用:给搜索工具检查生成清楚的摘要,说明搜索命令是否可用、来自哪里、具体命令是什么。

数据流:输入是 search 检查项。它读取 provider、command、readiness;如果检查正常且三者都有,就拼成一句带命令的说明,否则回退到原摘要。

调用关系display_summary 处理 search 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

git_summary662–666 ↗
fn git_summary(check: &DoctorCheck) -> String

作用:给 Git 环境检查生成短摘要,优先显示 Git 版本。

数据流:输入是 git 检查项。它先读 git version,没有就读 selected git,再没有就返回原摘要。

调用关系display_summary 处理 git 类别时调用它。

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

terminal_summary668–685 ↗
fn terminal_summary(check: &DoctorCheck) -> String

作用:把终端相关信息压成一行,比如终端名、版本、多路复用器和 TERM 变量。

数据流:输入是 terminal 检查项。它收集终端名称和版本、multiplexer、TERM 等细节;有内容就用点号分隔拼成一行,没有就用原摘要。

调用关系display_summary 处理 terminal 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 2 个(new, format!)。

title_summary687–698 ↗
fn title_summary(check: &DoctorCheck, options: HumanOutputOptions) -> String

作用:生成终端标题设置的摘要,说明标题来源和项目名显示值。

数据流:输入是 title 检查项和显示选项。它读取标题来源和项目值;两者都有就按 Unicode 或 ASCII 分隔符拼起来,只有来源就返回来源,否则用原摘要。

调用关系display_summary 处理 title 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

state_summary700–714 ↗
fn state_summary(check: &DoctorCheck) -> String

作用:给本地状态数据库生成健康摘要。如果几个数据库完整性都正常,就显示“databases healthy”。

数据流:输入是 state 检查项。它检查 state、log、goals、memories 四个数据库完整性细节是否都是 ok;若整体状态也正常,就返回健康文案,否则返回原摘要。

调用关系display_summary 处理 state 类别时调用它。

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

mcp_summary716–739 ↗
fn mcp_summary(check: &DoctorCheck) -> String

作用:汇总 MCP 服务器配置数量。MCP 可以理解成 Codex 能连接的外部工具/服务接口。

数据流:输入是 mcp 检查项。它读取配置的服务器数、禁用数,并从细节里提取不同传输方式的服务器数量;输出一句数量摘要,缺少关键数据就回退原摘要。

调用关系display_summary 处理 mcp 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

sandbox_summary741–751 ↗
fn sandbox_summary(check: &DoctorCheck) -> String

作用:把沙箱配置压成一行,说明文件系统、网络和审批策略分别是什么。

数据流:输入是 sandbox 检查项。它读取 approval policy、filesystem sandbox、network sandbox;三者齐全就拼成摘要,否则用原摘要。

调用关系display_summary 处理 sandbox 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

network_summary753–763 ↗
fn network_summary(check: &DoctorCheck) -> String

作用:给网络环境检查生成简短说明,重点说明有没有代理环境变量。

数据流:输入是 network 检查项。它读取 proxy env vars;值为 none 就说没有代理变量,否则说存在代理变量;读不到就返回原摘要。

调用关系display_summary 处理 network 类别时调用它。

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

websocket_summary765–774 ↗
fn websocket_summary(check: &DoctorCheck) -> String

作用:给 WebSocket 连接检查生成短摘要。WebSocket 是一种长连接通信方式,这里关心握手是否成功和超时时间。

数据流:输入是 websocket 检查项。它读取握手结果或状态,以及连接超时值,并把毫秒文字稍微压短;两者都有就拼成连接摘要,否则返回原摘要。

调用关系display_summary 处理 websocket 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

app_server_summary776–783 ↗
fn app_server_summary(check: &DoctorCheck) -> String

作用:给后台服务检查生成一行状态说明,比如是否运行、运行模式是什么。

数据流:输入是 app-server 检查项。它读取 status 和 mode;两者都有就拼成“状态(模式)”,否则用原摘要。

调用关系display_summary 处理 app-server 类别时调用它。

调用图:调用 1 个内部函数(detail_value);被 1 处调用(display_summary);外部调用 1 个(format!)。

separator785–791 ↗
fn separator(options: HumanOutputOptions) -> String

作用:生成报告里的横向分隔线。根据用户环境,它可以用 Unicode 长横线,也可以用普通减号。

数据流:输入是显示选项。若 ASCII 模式开启,就重复 -;否则重复 ;输出固定宽度的分隔线字符串。

调用关系:报告标题区和总结区之间需要分隔线时会用它。

highlight_actions793–813 ↗
fn highlight_actions(text: &str, options: HumanOutputOptions) -> String

作用:高亮用户可能需要复制或执行的内容,比如反引号里的命令和 --json 这类参数。

数据流:输入是一段文字和显示选项。没开颜色就原样处理;开颜色时,它按反引号切分,代码片段染成青色,普通文字再交给 highlight_flags 高亮命令行参数。

调用关系style_description 和细节/建议显示会借助它让可操作内容更显眼。

调用图:调用 2 个内部函数(cyan, highlight_flags);被 1 处调用(style_description);外部调用 1 个(new)。

highlight_flags815–830 ↗
fn highlight_flags(text: &str, options: HumanOutputOptions) -> String

作用:在普通文字里找出 --xxx 这样的命令行参数并高亮。

数据流:输入是一段普通文字。它按空白保留切分每个词,剥掉末尾标点判断是否以 -- 开头;是就高亮后再把标点和空白接回去。

调用关系highlight_actions 在处理非代码文字时调用它。

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

redact_detail832–860 ↗
fn redact_detail(detail: &str) -> String

作用:把一条诊断细节里的敏感内容遮住。它是防止报告泄露 API key、token、URL 凭据的重要安全阀。

数据流:输入是一条细节字符串。它先判断是不是环境变量或安全的“存在/不存在”值;若包含常见密钥关键词,就只保留标签并写 <redacted>;否则会清理 URL 中的用户名密码、查询参数和深层路径。输出是脱敏后的字符串。

调用关系:详细输出和 JSON 脱敏相关流程会用它,测试专门覆盖 URL、密钥名和布尔存在值的情况。

调用图:调用 1 个内部函数(redact_urls);被 4 处调用(redact_detail_sanitizes_secret_url_path_segments, redact_detail_sanitizes_urls, redacted_json_issue, structured_json_details);外部调用 1 个(format!)。

is_safe_presence_value862–867 ↗
fn is_safe_presence_value(value: &str) -> bool

作用:判断一个值是否只是“有/没有、真/假”这类安全状态。这样的值可以显示,不算泄密。

数据流:输入是字符串值。它去掉空白并转小写,再判断是否等于 true、false、present、missing 等白名单词;输出布尔值。

调用关系redact_detail 用它决定某些看起来敏感的字段是否可以原样保留状态。

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

redact_urls869–874 ↗
fn redact_urls(detail: &str) -> String

作用:扫描一整条文字,把里面出现的 URL 一个个脱敏。

数据流:输入是一条字符串。它按空白保留切分,每个片段交给 redact_url_token 处理,再重新拼回完整字符串。

调用关系redact_detail 在需要清理 URL 时调用它。

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

redact_url_token876–914 ↗
fn redact_url_token(token: &str) -> String

作用:脱敏单个可能是 URL 的词。它会去掉用户名密码、查询参数、片段标识,并隐藏深层路径。

数据流:输入是一个文本片段。找不到 :// 就原样返回;找到后保留协议和主机,去掉 user:pass@,只保留安全路径部分,并保留末尾标点;输出脱敏后的片段。

调用关系redact_urls 对每个词调用它,它再把路径处理交给 redact_url_path

调用图:调用 1 个内部函数(redact_url_path);外部调用 2 个(format!, matches!)。

redact_url_path916–926 ↗
fn redact_url_path(path: &str) -> String

作用:隐藏 URL 路径里可能带有令牌或 ID 的深层部分。比如 /mcp/abc123 会变成 /mcp/<redacted>

数据流:输入是 URL 的路径部分。没有路径或只有一级路径就原样返回;超过一级就保留第一段,把后面替换为 <redacted>

调用关系redact_url_token 在拆出 URL 路径后调用它。

调用图:被 1 处调用(redact_url_token);外部调用 1 个(format!)。

StatusCounts::from_report938–953 ↗
fn from_report(report: &DoctorReport, notes: usize) -> Self

作用:统计底部汇总需要的各类数量,比如正常、空闲、警告、失败和 Notes 数。

数据流:输入是报告和已经算出的 Notes 数。它从默认计数开始,逐项检查报告里的检查项,按 display_status 的结果累加;输出一个计数结构。

调用关系summary_line 调用它来生成最后的统计行。

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

bold956–962 ↗
fn bold(text: &str, options: HumanOutputOptions) -> String

作用:按需把文字加粗。没开颜色/样式时,它会原样返回,避免输出奇怪控制符。

数据流:输入是文字和显示选项。若允许颜色样式,就调用终端样式库加粗;否则返回原字符串。

调用关系:标题、分组名等需要强调的位置会使用它。

dim964–970 ↗
fn dim(text: &str, options: HumanOutputOptions) -> String

作用:按需把文字变淡,用来降低辅助信息的视觉重量。

数据流:输入是文字和显示选项。开样式就加 dim 效果,不开就原样返回。

调用关系:状态标记、汇总数字、正常摘要、括号说明等多处都会用它。

调用图:被 5 处调用(count_label, status_marker, style_description, style_detail_bare_token, summary_line)。

very_dim972–974 ↗
fn very_dim(text: &str, options: HumanOutputOptions) -> String

作用:生成更淡的文字颜色,常用于不重要的小符号。

数据流:输入是文字和显示选项。它使用 256 色里的较暗灰色;没开颜色时原样返回。

调用关系:细节项目符号等低优先级内容会用它。

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

detail_label976–978 ↗
fn detail_label(text: &str, options: HumanOutputOptions) -> String

作用:给细节行左侧标签上灰色,让标签不抢正文值的注意力。

数据流:输入是标签文字和显示选项。它用固定灰色处理,或在无颜色时原样返回。

调用关系write_detail_line 写详细信息时使用它。

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

detail_value980–985 ↗
fn detail_value(text: &str, options: HumanOutputOptions) -> String

作用:给细节行右侧的值做细粒度高亮,比如把 ok 变绿、路径或 URL 变青、缺失值变灰。

数据流:输入是细节值和显示选项。没开颜色就原样返回;开颜色就交给 style_detail_text 分词处理。

调用关系write_detail_line 用它显示详细值,测试也直接检查它的颜色规则。

调用图:调用 1 个内部函数(style_detail_text);被 2 处调用(detail_value_colors_inline_statuses_and_low_signal_values, write_detail_line)。

style_detail_text987–1003 ↗
fn style_detail_text(text: &str, options: HumanOutputOptions) -> String

作用:处理一整段细节值,支持反引号里的代码片段高亮。

数据流:输入是文本和选项。它按反引号切开,代码片段染青色,普通片段交给 style_detail_plain_text;输出样式化文本。

调用关系detail_value 在颜色开启时调用它。

调用图:调用 2 个内部函数(cyan, style_detail_plain_text);被 1 处调用(detail_value);外部调用 1 个(new)。

style_detail_plain_text1005–1009 ↗
fn style_detail_plain_text(text: &str, options: HumanOutputOptions) -> String

作用:给普通细节文本按词做样式处理,同时保留原来的空白。

数据流:输入是普通文本。它按空白保留切分,每个 token 交给 style_detail_token;输出重新拼好的字符串。

调用关系style_detail_text 处理非代码片段时调用它。

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

style_detail_token1011–1018 ↗
fn style_detail_token(token: &str, options: HumanOutputOptions) -> String

作用:给一个细节词处理样式,同时保留末尾标点和空格。

数据流:输入是一个 token。它拆出主体、标点和空白后,把主体交给 style_detail_bare_token,最后再拼回标点和空白。

调用关系style_detail_plain_text 对每个词调用它。

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

style_detail_bare_token1020–1045 ↗
fn style_detail_bare_token(bare: &str, options: HumanOutputOptions) -> String

作用:决定单个无标点词该用什么颜色。它识别 <redacted>、缺失值、ok、路径、URL、命令参数、单位等常见模式。

数据流:输入是一个干净词和显示选项。它按规则判断并返回灰色、绿色、青色或普通文字;如果词为空就返回空字符串。

调用关系style_detail_token 调用它完成真正的词级样式判断。

调用图:调用 6 个内部函数(color256, cyan, is_falsy, dim, green, looks_copyable);被 1 处调用(style_detail_token);外部调用 3 个(new, format!, matches!)。

green1047–1049 ↗
fn green(text: &str, options: HumanOutputOptions) -> String

作用:把文字染成绿色,通常表示正常或成功。

数据流:输入是文字和显示选项。它调用 color256 使用绿色编号;没开颜色则原样返回。

调用关系:状态标记、汇总标签和细节里的 ok 会用它。

调用图:调用 1 个内部函数(color256);被 3 处调用(count_label, status_marker, style_detail_bare_token)。

amber1051–1053 ↗
fn amber(text: &str, options: HumanOutputOptions) -> String

作用:把文字染成琥珀色,通常用于更新提示或较温和的提醒。

数据流:输入是文字和显示选项。它通过 color256 使用琥珀色编号;无颜色时原样返回。

调用关系:更新状态、更新提醒和部分汇总标签会用它。

调用图:调用 1 个内部函数(color256);被 4 处调用(count_label, status_marker, style_description, style_update_note_summary)。

orange1055–1057 ↗
fn orange(text: &str, options: HumanOutputOptions) -> String

作用:把文字染成橙色,主要表示警告或需要注意。

数据流:输入是文字和显示选项。它通过 color256 使用橙色编号;无颜色时原样返回。

调用关系:警告标记、问题箭头和 warn/notes 标签会用它。

调用图:调用 1 个内部函数(color256);被 3 处调用(count_label, detail_marker, status_marker)。

red1059–1061 ↗
fn red(text: &str, options: HumanOutputOptions) -> String

作用:把文字染成红色,表示失败或严重问题。

数据流:输入是文字和显示选项。它通过 color256 使用红色编号;无颜色时原样返回。

调用关系:失败标记和 fail 汇总标签会用它。

调用图:调用 1 个内部函数(color256);被 2 处调用(count_label, status_marker)。

cyan1063–1065 ↗
fn cyan(text: &str, options: HumanOutputOptions) -> String

作用:把文字染成青色,通常表示命令、参数、路径或 URL 这类可复制内容。

数据流:输入是文字和显示选项。它通过 color256 使用青色编号;无颜色时原样返回。

调用关系highlight_actionsstyle_detail_textstyle_detail_bare_token 会用它突出可操作文本。

调用图:调用 1 个内部函数(color256);被 3 处调用(highlight_actions, style_detail_bare_token, style_detail_text)。

color2561067–1073 ↗
fn color256(text: &str, code: u8, options: HumanOutputOptions) -> String

作用:统一处理 256 色终端颜色。它是本文件所有具体颜色函数的底层小工具。

数据流:输入是文字、颜色编号和显示选项。开颜色时用终端颜色库转换编号并上色;不开颜色时原样返回。

调用关系:green、amber、orange、red、cyan、detail_label、very_dim 等颜色包装函数都调用它。

调用图:被 8 处调用(amber, cyan, detail_label, green, orange, red, style_detail_bare_token, very_dim);外部调用 1 个(from)。

looks_copyable1075–1083 ↗
fn looks_copyable(text: &str) -> bool

作用:判断一段文字看起来是不是用户可能要复制的路径或链接。

数据流:输入是一个词。它检查是否以 http、https、wss、~/、/、./、../ 等常见路径或链接前缀开头;输出布尔值。

调用关系style_detail_bare_token 用它把可复制的值染成青色。

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

tests::detailed_no_color_unicode_options1091–1098 ↗
fn detailed_no_color_unicode_options() -> HumanOutputOptions

作用:构造测试用显示选项:显示详细信息、用 Unicode 符号、不启用颜色。

数据流:没有输入。它返回一个固定的 HumanOutputOptions,供测试复用。

调用关系:多个渲染测试用它保证预期字符串不含颜色控制码。

tests::summary_no_color_unicode_options1100–1107 ↗
fn summary_no_color_unicode_options() -> HumanOutputOptions

作用:构造测试用显示选项:只显示摘要、用 Unicode 符号、不启用颜色。

数据流:没有输入。它返回固定选项,表示摘要模式。

调用关系:摘要输出、Notes 提升等测试会调用它。

tests::detailed_all_no_color_unicode_options1109–1116 ↗
fn detailed_all_no_color_unicode_options() -> HumanOutputOptions

作用:构造测试用显示选项:详细模式并展开所有可截断列表,不启用颜色。

数据流:没有输入。它返回 show_details=trueshow_all=true 的固定选项。

调用关系:功能标志列表展开测试用它对比普通详细模式。

tests::detailed_color_unicode_options1118–1125 ↗
fn detailed_color_unicode_options() -> HumanOutputOptions

作用:构造测试用显示选项:详细模式、Unicode 符号、启用颜色。

数据流:没有输入。它返回一个开启颜色的固定选项。

调用关系:颜色高亮相关测试用它检查是否产生终端颜色控制码。

tests::sample_report1127–1237 ↗
fn sample_report() -> DoctorReport

作用:生成一份覆盖多种类别和状态的示例医生报告。它像测试里的标准样本病例。

数据流:没有输入。它创建系统、运行时、git、终端、认证、联网、后台服务等检查项,最后组装成 DoctorReport 返回。

调用关系:多数渲染测试调用它,避免每个测试重复搭建同样的报告。

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

tests::render_human_report_includes_details_by_default_without_color1240–1300 ↗
fn render_human_report_includes_details_by_default_without_color()

作用:测试详细模式下的完整无颜色输出是否符合预期。

数据流:它用样本报告和详细无颜色选项调用 render_human_report,再和手写的期望大字符串比较;测试通过表示排版、细节、总结都一致。

调用关系:这是覆盖主渲染路径的重要测试。

调用图:调用 1 个内部函数(render_human_report);外部调用 4 个(assert_eq!, detailed_no_color_unicode_options, sample_report, format!)。

tests::render_human_report_snapshot_covers_environment_rows1303–1308 ↗
fn render_human_report_snapshot_covers_environment_rows()

作用:用快照测试固定环境分组的输出样子。快照测试就是把当前输出保存成基准,以后变了会提醒。

数据流:它渲染样本报告,然后交给快照断言;如果输出和已保存快照不同,测试会失败。

调用关系:它补充普通字符串断言,帮助发现环境行排版变化。

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

tests::render_human_report_supports_summary_output_without_color1311–1355 ↗
fn render_human_report_supports_summary_output_without_color()

作用:测试摘要模式下不会显示详细细节,并且底部提示正确。

数据流:它用样本报告和摘要无颜色选项渲染,再与期望字符串逐字比较。

调用关系:它直接验证 render_human_reportshow_details=false 时的分支。

调用图:调用 1 个内部函数(render_human_report);外部调用 4 个(assert_eq!, sample_report, summary_no_color_unicode_options, format!)。

tests::render_human_report_includes_threads_row_in_environment1358–1377 ↗
fn render_human_report_includes_threads_row_in_environment()

作用:测试新增的 threads 类别会出现在 Environment 分组里。

数据流:它在样本报告里追加一个 threads 警告检查,渲染摘要输出,然后查找包含 threads 的行并确认摘要存在。

调用关系:它保护 GROUPS 里的 threads 分类不被误删。

调用图:调用 2 个内部函数(new, render_human_report);外部调用 3 个(assert!, sample_report, summary_no_color_unicode_options)。

tests::render_human_report_includes_memories_db_in_state_health_summary1380–1408 ↗
fn render_human_report_includes_memories_db_in_state_health_summary()

作用:测试 state 健康摘要会把 memories 数据库也算进去。

数据流:它构造一份只有 state 检查的报告,并提供四个数据库完整性 ok 的细节;渲染后确认主行显示 databases healthy,细节里也有 memories DB。

调用关系:它验证 state_summary 和 detail 展示对 memories DB 的支持。

调用图:调用 1 个内部函数(render_human_report);外部调用 3 个(assert!, detailed_no_color_unicode_options, vec!)。

tests::render_human_report_supports_ascii_output1411–1463 ↗
fn render_human_report_supports_ascii_output()

作用:测试纯 ASCII 输出模式,适合不支持 Unicode 符号的终端。

数据流:它用样本报告和 ascii 选项渲染,再比较期望字符串,确认对勾、警告、分隔线和分隔符都换成 ASCII。

调用关系:它覆盖 status_markerseparatortitle_summary 等 ASCII 分支。

调用图:调用 1 个内部函数(render_human_report);外部调用 3 个(assert_eq!, sample_report, format!)。

tests::render_human_report_includes_redacted_details1466–1477 ↗
fn render_human_report_includes_redacted_details()

作用:测试详细输出中会显示安全的脱敏/存在性信息,比如 API key 是否存在。

数据流:它渲染样本报告的详细模式,然后确认输出包含 OPENAI_API_KEY present 这类安全状态。

调用关系:它间接保护细节渲染和脱敏策略的配合。

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

tests::render_human_report_explains_terminal_warning_issue1480–1516 ↗
fn render_human_report_explains_terminal_warning_issue()

作用:测试终端宽度警告会显示具体问题、期望值和修复建议。

数据流:它构造一个 terminal 警告报告,带 issue、expected 和 remedy;渲染后检查主行、问题细节和箭头建议都出现,并确认不会用普通终端摘要盖掉问题。

调用关系:它验证 row_descriptionissue_summarywrite_detail_line 的问题展示路径。

调用图:调用 1 个内部函数(render_human_report);外部调用 3 个(assert!, detailed_no_color_unicode_options, vec!)。

tests::render_human_report_promotes_notes_without_changing_statuses1519–1594 ↗
fn render_human_report_promotes_notes_without_changing_statuses()

作用:测试一些特殊情况会被提升到 Notes,但不会乱改原本检查状态。

数据流:它构造有新版本、大量 rollout、沙箱不严、MCP 警告、认证模式混杂、后台服务空闲的报告;渲染后检查 Notes 和底部统计是否符合预期。

调用关系:它集中覆盖 notes_for_reportupdate_noterollout_notesandbox_noteauth_reachability_notedisplay_status

调用图:调用 1 个内部函数(render_human_report);外部调用 3 个(assert!, summary_no_color_unicode_options, vec!)。

tests::render_human_report_expands_feature_flags_with_all1597–1623 ↗
fn render_human_report_expands_feature_flags_with_all()

作用:测试 --all 会展开功能标志列表,而普通详细模式只显示压缩摘要。

数据流:它构造 config 报告,分别用普通详细选项和 show_all 选项渲染,再检查紧凑输出不含完整列表、展开输出含完整列表。

调用关系:它验证本文件与 detail 子模块的列表截断/展开行为配合正确。

调用图:调用 1 个内部函数(render_human_report);外部调用 4 个(assert!, detailed_all_no_color_unicode_options, detailed_no_color_unicode_options, vec!)。

tests::detail_value_colors_inline_statuses_and_low_signal_values1626–1637 ↗
fn detail_value_colors_inline_statuses_and_low_signal_values()

作用:测试细节值里的 ok、unknown、路径和 <redacted> 等会被正确着色。

数据流:它调用 detail_value 处理一段混合文本,然后检查输出里包含预期的终端颜色码。

调用关系:它直接覆盖 detail_valuestyle_detail_bare_token 的颜色规则。

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

tests::update_note_emphasizes_available_version_and_dims_context1640–1648 ↗
fn update_note_emphasizes_available_version_and_dims_context()

作用:测试更新提醒会突出新版本号,并把括号里的上下文变淡。

数据流:它用开启颜色的选项调用 style_update_note_summary,然后检查新版本部分和括号部分的颜色/变淡控制码。

调用关系:它保护更新 Notes 的特殊样式。

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

tests::redact_detail_sanitizes_urls1651–1660 ↗
fn redact_detail_sanitizes_urls()

作用:测试 URL 中的用户名密码、查询参数和片段会被清理。

数据流:它把含 user:pass@、查询参数和 fragment 的 URL 传给 redact_detail,再确认输出只保留安全的主机和路径。

调用关系:它覆盖 redact_detailredact_urlsredact_url_token 的核心脱敏行为。

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

tests::redact_detail_sanitizes_secret_url_path_segments1663–1670 ↗
fn redact_detail_sanitizes_secret_url_path_segments()

作用:测试 URL 深层路径会被隐藏,避免路径段里带秘密 ID 时泄露。

数据流:它传入带两级路径的 URL,确认输出把第二级替换成 <redacted>

调用关系:它主要保护 redact_url_path 的规则。

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

tests::redact_detail_preserves_env_var_names1673–1678 ↗
fn redact_detail_preserves_env_var_names()

作用:测试环境变量名字本身不会被误删。知道哪些变量存在通常有用,而且不等于泄露变量值。

数据流:它断言一条只列出 API key 环境变量名称的文字保持不变。

调用关系:它保护 redact_detail 对 env var 标签的特殊处理。

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

tests::redact_detail_preserves_secret_presence_booleans1681–1690 ↗
fn redact_detail_preserves_secret_presence_booleans()

作用:测试敏感字段的 true/false 存在性状态可以保留。

数据流:它分别检查 stored ChatGPT tokens: truefalse 经过脱敏后仍原样返回。

调用关系:它保护 is_safe_presence_valueredact_detail 的配合。

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

tests::render_human_report_can_emit_color1693–1704 ↗
fn render_human_report_can_emit_color()

作用:测试开启颜色时,渲染结果确实包含终端颜色控制码。

数据流:它用样本报告和 color_enabled=true 调用 render_human_report,然后检查输出包含 ANSI 转义开头。

调用关系:它验证颜色开关能贯穿主渲染流程。

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

cli/src/doctor/output/detail.rs源码 ↗
domain_logicdoctor output rendering

doctor 检查会先把各种信息存成简单的“名字: 值”文本,这样既方便输出 JSON,也方便给人看。但人直接看这些原始文本会很累。这个文件就像一位排版编辑:先把详情拆成标签和值,再按检查类别整理,比如系统、运行环境、安装、Git、配置、状态数据库等;然后把重要信息放前面,把重复或已处理过的信息收尾补上;再把长路径缩短、时间戳改成更容易读的格式,还会把问题里的“期望值”和“修复建议”贴到对应行上。它还支持 --all:默认只展示前几项,用户想看完整列表时再展开。

函数细节37
detail_lines37–56 ↗
fn detail_lines(check: &DoctorCheck, options: HumanOutputOptions) -> Vec<HumanDetail>

作用:这是本文件的总入口,把一个 doctor 检查结果变成一组可以打印给人的详情行。终端输出每个检查项时会用它。

数据流:输入是一个检查结果和显示选项 → 先解析原始详情,再按检查类别选择对应的整理方式,之后补上问题的期望值、把路径和时间变好读,最后追加修复建议 → 输出一组 HumanDetail 行,不直接打印,但给后续打印函数使用。

调用关系:它由 write_check_row 在写单个检查项时调用。它把具体整理工作分给 system_details、runtime_details、install_details、git_details、config_details、state_details、generic_details 等函数,最后再调用 issue_remedies 补修复建议。

调用图:调用 10 个内部函数(config_details, generic_details, git_details, install_details, issue_remedies, parsed_details, runtime_details, state_details, system_details, title_details);被 1 处调用(write_check_row)。

system_details58–92 ↗
fn system_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail>

作用:整理“系统环境”类检查的详情,比如操作系统、语言环境、编辑器、分页器这些用户环境信息。

数据流:输入是已经拆好的详情列表 → 按固定顺序挑出常见系统字段,换成更适合展示的名字,再把没被挑走的其他字段补到末尾 → 输出整理后的详情行。

调用关系:它由 detail_lines 在检查类别是 system 时调用。它主要依靠 push_row_if_present 放入指定字段,再用 push_remaining 防止遗漏其他信息。

调用图:调用 2 个内部函数(push_remaining, push_row_if_present);被 1 处调用(detail_lines);外部调用 1 个(new)。

detail_value94–99 ↗
fn detail_value(check: &DoctorCheck, label: &str) -> Option<String>

作用:从一个检查结果里按标签取出某个具体值。其他摘要函数想快速拿到某项详情时会用它。

数据流:输入是检查结果和要找的标签名 → 先把原始详情解析成标签和值,再找到第一个标签完全匹配的项目 → 输出这个值;找不到就输出空值。

调用关系:它被 app_server_summary、auth_reachability_note、git_summary、mcp_summary、network_summary、rollout_note、runtime_summary、sandbox_note 等摘要或提示函数调用。它内部复用 parsed_details,保证取值前也会走同一套脱敏和解析规则。

调用图:调用 1 个内部函数(parsed_details);被 15 处调用(app_server_summary, auth_reachability_note, git_summary, mcp_summary, network_summary, rollout_note, runtime_summary, sandbox_note, sandbox_summary, search_summary (+5 more))。

rollout_summary101–114 ↗
fn rollout_summary(value: &str) -> Option<String>

作用:把 rollout 文件统计这种长字符串,改写成更短、更适合人看的摘要。

数据流:输入类似“多少 files, 多少 total bytes, 多少 average bytes”的文本 → 拆出文件数、总字节数、平均字节数,并把数字和字节单位格式化 → 输出类似“1,234 files · 12.34 MB (avg 1.00 KB)”的摘要;格式不对就输出空值。

调用关系:它由 state_details 在展示状态文件统计时调用。它会用 format_count 给数量加逗号,用 format_bytes 把字节数换成人熟悉的 KB、MB、GB。

调用图:被 1 处调用(state_details);外部调用 1 个(format!)。

rollout_files_and_bytes116–123 ↗
fn rollout_files_and_bytes(value: &str) -> Option<(u64, u64)>

作用:从 rollout 统计文本里抽出“文件数”和“总字节数”这两个数字,给其他提示逻辑做判断。

数据流:输入是一段 rollout 统计文本 → 按固定格式拆出文件数和总字节数,并转成数字 → 输出一对数字;如果文本格式不对或数字解析失败,就输出空值。

调用关系:它被 rollout_note 调用,用来判断 rollout 文件规模并生成更有针对性的说明。它只负责提取数字,不负责展示。

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

format_bytes125–140 ↗
fn format_bytes(bytes: u64) -> String

作用:把字节数换成更容易读的容量单位,比如 B、KB、MB、GB。

数据流:输入是一个字节数量 → 根据大小选择合适单位,并保留两位小数 → 输出格式化后的字符串。

调用关系:它被 rollout_summary 用来把总大小和平均大小变成人能快速理解的数字。

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

format_count142–158 ↗
fn format_count(count: u64) -> String

作用:把大数字加上千位分隔符,让文件数量更好读。

数据流:输入是一个整数 → 从末尾每三位切一段并插入逗号 → 输出类似“12,345,678”的字符串。

调用关系:它被 rollout_summary 调用,用在 rollout 文件数量展示里。

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

parsed_details160–178 ↗
fn parsed_details(check: &DoctorCheck) -> Vec<ParsedDetail>

作用:把检查结果里的原始详情字符串拆成“标签”和“值”。这是后面所有美化工作的基础。

数据流:输入是 DoctorCheck 里的详情列表 → 每条先做脱敏,避免敏感信息原样显示,再按“: ”分成标签和值;没有冒号的就当成普通说明 → 输出 ParsedDetail 列表。

调用关系:它被 detail_lines 和 detail_value 调用。detail_lines 用它做整页展示,detail_value 用它做单项查找。

调用图:被 2 处调用(detail_lines, detail_value)。

runtime_details180–199 ↗
fn runtime_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail>

作用:整理程序自身运行环境的信息,比如版本、安装方式、提交号、当前可执行文件。

数据流:输入是解析后的详情 → 按预定顺序挑出运行时关键字段,给部分字段改展示名,再追加剩余未展示字段 → 输出人可读的详情行。

调用关系:它由 detail_lines 在类别是 runtime 时调用。它和 system_details 一样,靠 push_row_if_present 和 push_remaining 配合完成“先重点、后补漏”。

调用图:调用 2 个内部函数(push_remaining, push_row_if_present);被 1 处调用(detail_lines);外部调用 1 个(new)。

install_details201–272 ↗
fn install_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec<HumanDetail>

作用:整理安装相关信息,尤其是程序是不是被 npm、bun 这类包管理器安装,以及 PATH 里找到的可执行文件位置。

数据流:输入是解析后的详情和显示选项 → 展示安装上下文,合并 npm、bun、包根目录状态,把 PATH 里的 codex 条目按数量列出;默认只列前三个,--all 才全列 → 输出整理好的安装详情行。

调用关系:它由 detail_lines 在类别是 install 时调用。它会用 numbered_values 收集编号条目,用 value 查普通字段,用 push_remaining 把没有专门处理的字段补上。

调用图:调用 4 个内部函数(numbered_values, push_remaining, push_row_if_present, value);被 1 处调用(detail_lines);外部调用 5 个(new, Bullet, Continuation, iter, format!)。

git_details274–331 ↗
fn git_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec<HumanDetail>

作用:整理 Git 相关检查信息,比如选中的 git、版本、仓库根目录、分支、PATH 里的 git 候选项。

数据流:输入是解析后的 Git 详情和显示选项 → 先放常用 Git 信息,再把 PATH git 条目合并成列表;默认只显示前三个,--all 展示完整列表 → 输出适合终端阅读的 Git 详情行。

调用关系:它由 detail_lines 在类别是 git 时调用。它调用 numbered_values 收集 PATH 项,调用 push_row_if_present 放关键字段,最后用 push_remaining 防止漏掉额外详情。

调用图:调用 3 个内部函数(numbered_values, push_remaining, push_row_if_present);被 1 处调用(detail_lines);外部调用 3 个(new, Continuation, format!)。

title_details333–363 ↗
fn title_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail>

作用:整理终端标题相关信息,说明标题内容来自哪里、包含哪些项目。

数据流:输入是解析后的标题详情 → 把标题来源、标题项目、活动项、项目来源和项目值按清楚的名字放入输出 → 再补上其他未消费字段 → 输出标题检查的详情行。

调用关系:它由 detail_lines 在类别是 title 时调用。它主要复用 push_row_if_present 和 push_remaining。

调用图:调用 2 个内部函数(push_remaining, push_row_if_present);被 1 处调用(detail_lines);外部调用 1 个(new)。

config_details365–418 ↗
fn config_details(parsed: &[ParsedDetail], options: HumanOutputOptions) -> Vec<HumanDetail>

作用:整理配置相关信息,比如当前模型、工作目录、配置文件读取和解析状态、MCP 服务器、功能开关。

数据流:输入是解析后的配置详情和显示选项 → 把模型和模型提供方合成一行,列出配置文件状态,再交给 push_feature_flags 处理功能开关,并把旧名字的功能开关标成 legacy alias → 输出配置详情行。

调用关系:它由 detail_lines 在类别是 config 时调用。它会调用 push_feature_flags 处理较复杂的功能开关展示,再用 push_remaining 保留其他配置字段。

调用图:调用 4 个内部函数(push_feature_flags, push_remaining, push_row_if_present, value);被 1 处调用(detail_lines);外部调用 2 个(new, iter)。

state_details420–464 ↗
fn state_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail>

作用:整理程序本地状态相关信息,比如 CODEX_HOME、日志目录、SQLite 数据库位置和 rollout 文件统计。

数据流:输入是解析后的状态详情 → 先展示关键目录,再把几个数据库路径和完整性检查结果合成行,最后把 active 和 archived rollout 文件统计改写成摘要 → 输出状态详情行。

调用关系:它由 detail_lines 在类别是 state 时调用。它把数据库行交给 push_database_row,把 rollout 统计交给 rollout_summary,末尾仍用 push_remaining 补漏。

调用图:调用 5 个内部函数(push_database_row, push_remaining, push_row_if_present, rollout_summary, value);被 1 处调用(detail_lines);外部调用 1 个(new)。

generic_details466–481 ↗
fn generic_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail>

作用:给没有专门分类规则的检查做通用展示。它保证未知类别也不会没内容可看。

数据流:输入是解析后的详情 → 有标签的变成普通行,没标签的变成项目符号说明 → 输出一组最朴素但可读的详情行。

调用关系:它由 detail_lines 在遇到未知类别时调用,是兜底方案。它不做复杂重排,只保持信息可见。

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

push_feature_flags483–513 ↗
fn push_feature_flags(
    out: &mut Vec<HumanDetail>,
    parsed: &[ParsedDetail],
    options: HumanOutputOptions,
)

作用:把功能开关信息整理成几行简洁说明。功能开关就是控制某些实验或可选能力是否启用的开关。

数据流:输入是输出列表、解析后的详情和显示选项 → 读取启用数量和覆盖设置,先写一行总览;如果有覆盖设置,就列出覆盖项;如果用户传了 --all,还会列出全部启用开关 → 直接修改输出列表。

调用关系:它由 config_details 调用。它把列表展示细节交给 push_list_row,把逗号分隔文本交给 list_items,把“name=value”简化成名字交给 override_names。

调用图:调用 4 个内部函数(list_items, override_names, push_list_row, value);被 1 处调用(config_details);外部调用 1 个(format!)。

push_list_row515–540 ↗
fn push_list_row(
    out: &mut Vec<HumanDetail>,
    label: &str,
    items: &[String],
    options: HumanOutputOptions,
)

作用:把一组字符串列表压成一行展示,并在太长时提示用户用 --all 查看完整列表。

数据流:输入是输出列表、行标签、项目列表和显示选项 → 根据是否 --all 决定显示全部还是只显示前几个,把项目用逗号连起来 → 往输出列表追加一行。

调用关系:它被 push_feature_flags 调用,用来展示功能开关覆盖项和启用项。它只关心列表怎么缩短,不关心列表内容是什么。

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

push_database_row542–556 ↗
fn push_database_row(out: &mut Vec<HumanDetail>, parsed: &[ParsedDetail], label: &str)

作用:把数据库路径和它的完整性检查结果合成一行,避免用户分开看两条零散信息。

数据流:输入是输出列表、解析详情和数据库标签 → 找到数据库路径,再查对应的“integrity”字段;如果有完整性结果,就拼在路径后面 → 往输出列表追加数据库详情行;没有路径就不做事。

调用关系:它由 state_details 调用,用来处理 state DB、log DB、goals DB、memories DB 等本地数据库。它内部用 value 查字段。

调用图:调用 1 个内部函数(value);被 1 处调用(state_details);外部调用 1 个(format!)。

push_row_if_present558–571 ↗
fn push_row_if_present(
    out: &mut Vec<HumanDetail>,
    parsed: &[ParsedDetail],
    source_label: &str,
    display_label: &str,
)

作用:如果某个字段存在,就把它作为一行加入输出。它是很多分类整理函数里的小帮手。

数据流:输入是输出列表、解析详情、原始标签名和展示标签名 → 查找原始标签对应的值 → 找到就追加一行,找不到就什么也不改。

调用关系:它被 config_details、git_details、install_details、runtime_details、state_details、system_details、title_details 反复调用,用来减少重复代码。

调用图:调用 1 个内部函数(value);被 7 处调用(config_details, git_details, install_details, runtime_details, state_details, system_details, title_details)。

push_remaining573–600 ↗
fn push_remaining(
    out: &mut Vec<HumanDetail>,
    parsed: &[ParsedDetail],
    consumed_labels: &[&str],
    consumed_prefixes: &[&str],
)

作用:把前面没有专门展示过的详情补到输出里,防止检查结果里的信息被整理过程漏掉。

数据流:输入是输出列表、全部解析详情、已消费标签和已消费前缀 → 跳过已处理字段和特定噪声信息,其余有标签的变成普通行,没标签的变成项目符号 → 修改输出列表。

调用关系:它被各类详情整理函数在末尾调用。它会用 display_label 把少数内部标签换成更好懂的名字。

调用图:调用 1 个内部函数(display_label);被 7 处调用(config_details, git_details, install_details, runtime_details, state_details, system_details, title_details);外部调用 1 个(Bullet)。

humanize_detail602–619 ↗
fn humanize_detail(detail: HumanDetail, options: HumanOutputOptions) -> HumanDetail

作用:把单条详情行里的值再美化一遍,比如缩短路径、改写时间。它不改变行的含义,只让它更好读。

数据流:输入是一条 HumanDetail 和显示选项 → 如果是普通行、续行或项目符号,就把其中的值交给 humanize_value;如果是修复建议就保持原样 → 输出美化后的详情行。

调用关系:它在 detail_lines 的后半段被用于每一条详情。它把具体判断交给 humanize_value。

调用图:调用 1 个内部函数(humanize_value);外部调用 3 个(Bullet, Continuation, Remedy)。

attach_issue_metadata621–636 ↗
fn attach_issue_metadata(detail: HumanDetail, check: &DoctorCheck) -> HumanDetail

作用:把检查发现的问题信息贴到对应详情行上,尤其是“期望值”。这样用户能看到当前值和应该是什么。

数据流:输入是一条详情行和整个检查结果 → 如果这条是普通行,就按行标签去问题列表里找匹配字段,并补上 expected;如果不是普通行就原样返回 → 输出带或不带期望值的详情行。

调用关系:它在 detail_lines 中用于每一条整理后的详情。它依赖 issue_expected_for_label 查找具体期望值。

issue_expected_for_label638–649 ↗
fn issue_expected_for_label(check: &DoctorCheck, label: &str) -> Option<String>

作用:根据一行的标签,去检查问题列表里找对应的“期望值”。

数据流:输入是检查结果和展示标签 → 遍历问题,比较问题字段的原名和展示名是否能对上当前行标签 → 找到就输出这个问题的 expected;找不到就输出空值。

调用关系:它由 attach_issue_metadata 调用。它让详情行不只是显示“现在是什么”,还可以显示“应该是什么”。

issue_remedies651–661 ↗
fn issue_remedies(check: &DoctorCheck) -> Vec<HumanDetail>

作用:收集检查问题里的修复建议,并去掉重复建议。

数据流:输入是检查结果 → 遍历所有问题,取出 remedy 修复建议,用集合记录已经见过的文本来去重 → 输出一组 Remedy 详情行。

调用关系:它由 detail_lines 在最后调用。这样修复建议会跟在详情后面,用户看完现状后马上知道下一步该做什么。

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

humanize_value663–671 ↗
fn humanize_value(value: &str, _options: HumanOutputOptions) -> String

作用:把单个值改成更适合人看的形式。目前主要处理路径和 UTC 时间戳。

数据流:输入是一段文本值 → 如果看起来像路径,就缩短路径;否则如果看起来像时间戳,就改成“日期 时间 UTC”;都不是就保持原样 → 输出处理后的文本。

调用关系:它由 humanize_detail 调用。它把路径判断交给 looks_like_path,把路径缩短交给 shorten_path_prefix,把时间转换交给 humanize_timestamp。

调用图:调用 3 个内部函数(humanize_timestamp, looks_like_path, shorten_path_prefix);被 1 处调用(humanize_detail)。

humanize_timestamp673–680 ↗
fn humanize_timestamp(value: &str) -> Option<String>

作用:把机器常用的 ISO 时间戳改成更短的人类读法。

数据流:输入是一段文本 → 检查它是否像以 Z 结尾的 UTC 时间戳,再拆出日期和前五位时间 → 输出类似“2024-01-01 12:30 UTC”的文本;不像时间戳就输出空值。

调用关系:它由 humanize_value 调用,只在值不是路径时尝试处理。

调用图:被 1 处调用(humanize_value);外部调用 1 个(format!)。

shorten_path_prefix682–690 ↗
fn shorten_path_prefix(value: &str) -> String

作用:把长路径压短,尤其把用户家目录换成 ~,并保留路径后面的括号说明。

数据流:输入是一段路径文本,可能带有“ (说明)”后缀 → 先分离路径和后缀,再把家目录前缀缩成 ~,最后对过长路径做中间截断 → 输出短一些但仍能辨认的路径文本。

调用关系:它由 humanize_value 在发现值像路径时调用。它内部依次使用 home_shortened_path 和 middle_truncate。

调用图:调用 2 个内部函数(home_shortened_path, middle_truncate);被 1 处调用(humanize_value);外部调用 1 个(format!)。

home_shortened_path692–702 ↗
fn home_shortened_path(path: &str) -> String

作用:把当前用户家目录路径替换成 ~,让路径更短也更隐私友好。

数据流:输入是路径 → 读取环境变量 HOME;如果路径正好是家目录,就输出“~”;如果路径在家目录下面,就输出“~/后续路径”;否则原样输出 → 返回改写后的路径。

调用关系:它由 shorten_path_prefix 调用。它会读取系统环境变量 HOME。

调用图:被 1 处调用(shorten_path_prefix);外部调用 2 个(var_os, format!)。

middle_truncate704–721 ↗
fn middle_truncate(value: &str, max_chars: usize) -> String

作用:把过长文本从中间截断,保留开头和结尾。这样用户还能认出路径来自哪里、指向哪里。

数据流:输入是一段文本和最大字符数 → 如果没超长就原样返回;如果超长,就取前半段和后半段,中间放一个省略号 → 输出缩短后的文本。

调用关系:它由 shorten_path_prefix 调用,用来控制终端里路径不要撑得太长。

调用图:被 1 处调用(shorten_path_prefix);外部调用 1 个(format!)。

looks_like_path723–728 ↗
fn looks_like_path(value: &str) -> bool

作用:判断一段文本是否像文件路径。只有像路径的值才会走路径缩短逻辑。

数据流:输入是一段文本 → 检查它是否以 /、~/、./ 或 ../ 开头 → 输出 true 或 false。

调用关系:它由 humanize_value 调用,是路径美化前的简单门卫。

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

numbered_values730–736 ↗
fn numbered_values(parsed: &[ParsedDetail], prefix: &str) -> Vec<String>

作用:收集一组带编号前缀的详情值,比如 PATH git #1、PATH git #2 这类项目。

数据流:输入是解析后的详情和标签前缀 → 找出所有标签以这个前缀开头的项目,取出它们的值 → 输出值列表。

调用关系:它被 git_details 和 install_details 调用,用来把 PATH 里的多个候选位置整理成列表展示。

调用图:被 2 处调用(git_details, install_details);外部调用 1 个(iter)。

value738–743 ↗
fn value(parsed: &'a [ParsedDetail], label: &str) -> Option<&'a str>

作用:从解析后的详情里按标签取一个值。它是本文件最常用的查表小工具。

数据流:输入是解析详情列表和标签名 → 找到第一个标签完全匹配的项目 → 输出它的值;没有就输出空值。

调用关系:它被 config_details、install_details、push_database_row、push_feature_flags、push_row_if_present、state_details 等调用,承担大量字段读取工作。

调用图:被 6 处调用(config_details, install_details, push_database_row, push_feature_flags, push_row_if_present, state_details);外部调用 1 个(iter)。

display_label745–753 ↗
fn display_label(label: &str) -> String

作用:把少数内部味道较重的标签换成更适合用户看的名字。

数据流:输入是原始标签 → 如果是几个已知标签,就替换成更短更清楚的说法;否则保持原标签 → 输出展示标签。

调用关系:它由 push_remaining 调用,用在那些没有被专门处理、但仍要展示出来的剩余字段上。

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

list_items755–765 ↗
fn list_items(value: &str) -> Vec<String>

作用:把逗号分隔的文本拆成列表,并把“none”“false”这类表示没有的值当成空列表。

数据流:输入是一段列表文本 → 先用 is_falsy 判断是不是等于“没有”;如果不是,就按逗号拆开、去掉空白和空项 → 输出字符串列表。

调用关系:它由 push_feature_flags 调用,用来解析功能开关列表和覆盖项列表。

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

override_names767–773 ↗
fn override_names(items: &[String]) -> Vec<String>

作用:把功能开关覆盖项里的名字提出来,去掉等号后面的具体值。

数据流:输入是类似“flag=true”的项目列表 → 每项如果有等号,就只保留等号前的名字;没有等号就保留原项 → 输出名字列表。

调用关系:它由 push_feature_flags 调用。这样展示覆盖项时更简洁,不把具体赋值都挤在一行里。

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

yes_no775–777 ↗
fn yes_no(value: &str) -> &'static str

作用:把 true/false 这种机器值换成 yes/no 这种更口语的显示。

数据流:输入是一段文本 → 如果正好是“true”就输出“yes”,其他情况输出“no” → 返回展示用单词。

调用关系:它在安装详情整理中用于显示 npm、bun 是否管理当前安装,让用户不用直接读布尔值。

is_falsy779–784 ↗
fn is_falsy(value: &str) -> bool

作用:判断一段文本是否表达“没有、未设置、不存在、未知”这类否定含义。

数据流:输入是一段文本 → 去掉前后空白并转成小写,再和一组常见否定词比较,比如 false、none、not set、missing、no、- → 输出 true 或 false。

调用关系:它被 list_items 用来把空列表识别出来,也被 style_detail_bare_token、update_note 等其他输出逻辑复用。

调用图:被 3 处调用(list_items, style_detail_bare_token, update_note);外部调用 1 个(matches!)。

exec/src/event_processor_with_jsonl_output.rs源码 ↗
io_transportstartup、main loop、shutdown

这个文件像一个“同声传译员”。Codex 后台会不断发来通知,比如开始执行命令、改了文件、工具调用完成、出现警告、这一轮对话结束等。这里的 EventProcessorWithJsonOutput 会把这些通知整理成 exec_events 里的统一事件,再用 JSONL(JSON Lines,一行一个 JSON 对象)打印到标准输出。这样脚本、编辑器或别的程序可以一行行读取,而不用猜屏幕文字是什么意思。它还会给每个事件分配稳定的 item_编号,记住哪些项目已经开始但还没结束,维护运行中的待办列表,记录 token 用量,并在结束时把最后一条助手消息保存到指定文件。特别要注意:普通警告会作为“错误样式的条目”输出但不会中断流程;真正失败时会清掉最终消息,避免把失败结果误当成成功答案。

函数细节20
EventProcessorWithJsonOutput::new82–93 ↗
fn new(last_message_path: Option<PathBuf>) -> Self

作用:创建一个新的 JSONL 事件处理器。调用方用它来准备好一套空白状态,之后才能开始接收服务器通知并输出 JSON 事件。

数据流:进去的是一个可选的“最后消息保存路径” → 它建立编号计数器、原始编号到输出编号的映射表、待办列表、token 用量和最终消息等空状态 → 出来的是一个可以投入使用的 EventProcessorWithJsonOutput。

调用关系:这是整个处理器的起点。运行会话和大量测试都会先调用它;之后服务器通知会交给 process_server_notification,警告会交给 process_warning,结束时再由 print_final_output 收尾。

调用图:被 32 处调用(failed_turn_does_not_overwrite_output_last_message_file, mcp_tool_call_result_preserves_meta_in_jsonl_event, runtime_warning_emits_a_non_fatal_error_item, run_exec_session, agent_message_item_started_is_ignored, agent_message_item_updates_final_message, collab_spawn_begin_and_end_emit_item_events, command_execution_started_and_completed_translate_to_thread_events, empty_reasoning_items_are_ignored, failed_turn_clears_stale_final_message (+15 more));外部调用 2 个(new, new)。

EventProcessorWithJsonOutput::final_message95–97 ↗
fn final_message(&self) -> Option<&str>

作用:取出当前记住的最终回答文本。它主要给测试或外部代码确认“最后助手说了什么”。

数据流:进去的是处理器当前状态 → 它查看内部保存的 final_message → 如果有,就返回一段只读文本;如果没有,就返回空。

调用关系:它不推动流程,只是一个查看窗口。别的步骤会在收到助手消息或回合完成时更新 final_message,这个函数负责把结果安全地暴露出来。

EventProcessorWithJsonOutput::next_item_id99–101 ↗
fn next_item_id(&self) -> String

作用:生成下一个输出事件用的编号,比如 item_0、item_1。这样外部读 JSON 的程序可以知道哪些开始、更新、完成事件说的是同一个东西。

数据流:进去的是处理器里的计数器 → 它用原子计数器(一种安全递增的数字)取当前值并加一 → 出来的是一个新的字符串编号,同时内部计数器前进一格。

调用关系:它是很多映射步骤的编号机。collect_thread_events、started_item_id 等在需要新条目时会找它要编号,保证输出事件有清楚的身份。

调用图:被 2 处调用(collect_thread_events, started_item_id);外部调用 1 个(format!)。

EventProcessorWithJsonOutput::emit104–115 ↗
fn emit(&self, event: ThreadEvent)

作用:把一个内部事件真正打印成一行 JSON。没有它,前面整理好的事件只停在内存里,外部工具看不到。

数据流:进去的是一个 ThreadEvent → 它尝试把事件序列化(把结构化数据变成 JSON 字符串)→ 成功就打印这行 JSON;如果转换失败,就打印一条说明序列化失败的 JSON 错误。

调用关系:它是输出口。print_config_summary、process_server_notification 和 process_warning 都会先生成事件,再把每个事件交给 emit 打到标准输出。

调用图:被 3 处调用(print_config_summary, process_server_notification, process_warning);外部调用 1 个(println!)。

EventProcessorWithJsonOutput::usage_from_last_total117–127 ↗
fn usage_from_last_total(&self) -> Usage

作用:把最近一次记录的 token 用量整理成输出格式。token 可以粗略理解成模型读写文字时的计费小单位。

数据流:进去的是处理器里保存的最近总用量 → 如果没有记录,就给出全零的默认用量;如果有,就抽出输入、缓存输入、输出和推理输出 token 数 → 出来的是 Usage 对象。

调用关系:它在回合成功完成时被 collect_thread_events 使用,用来给 TurnCompleted 事件附上本轮消耗。用量本身来自之前的 ThreadTokenUsageUpdated 通知。

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

EventProcessorWithJsonOutput::map_todo_items129–139 ↗
fn map_todo_items(plan: &[codex_app_server_protocol::TurnPlanStep]) -> Vec<TodoItem>

作用:把服务器给的计划步骤转换成输出里的待办事项。它让外部界面能显示“正在做哪些步骤、哪些已经完成”。

数据流:进去的是一组计划步骤,每步有文字和状态 → 它逐条取出步骤文字,并把 Completed 状态变成 completed=true → 出来的是 TodoItem 列表。

调用关系:当 collect_thread_events 收到 TurnPlanUpdated 时会用它。之后这些待办项会被包装成“待办列表开始”或“待办列表更新”事件输出。

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

EventProcessorWithJsonOutput::map_item_with_id141–316 ↗
fn map_item_with_id(
        item: ThreadItem,
        make_id: impl FnOnce() -> String,
    ) -> Option<ExecThreadItem>

作用:把服务器协议里的 ThreadItem 翻译成 exec 输出协议里的 ExecThreadItem,并给它一个输出编号。它是不同事件格式之间最核心的翻译表。

数据流:进去的是一个原始线程条目和一个生成编号的方法 → 它按条目类型分别转换:助手消息、推理摘要、命令执行、文件改动、MCP 工具调用、协作代理工具调用、网页搜索等都会变成对应的输出细节;空白推理摘要会被丢弃 → 出来的是可输出条目,或者因为不适合输出而返回空。

调用关系:它被开始条目和完成条目的映射函数间接使用。collect_thread_events 不直接关心各种底层字段怎么改名、怎么归类,而是把翻译工作交给这个函数。

调用图:外部调用 9 个(AgentMessage, CollabToolCall, CommandExecution, FileChange, McpToolCall, Reasoning, WebSearch, from_value, to_value)。

EventProcessorWithJsonOutput::started_item_id318–326 ↗
fn started_item_id(&mut self, raw_id: &str) -> String

作用:为“已经开始但还没完成”的原始条目找到或创建一个输出编号。这样开始事件和完成事件能对上号。

数据流:进去的是服务器原始条目的 raw_id → 它先查映射表,已有编号就复用;没有就调用 next_item_id 生成新编号,并把 raw_id 和新编号记下来 → 出来的是这个条目的输出编号。

调用关系:map_started_item 会用它处理 ItemStarted。之后 map_completed_item_mut 会通过 completed_item_id 取回并移除同一个编号,形成“开始到完成”的闭环。

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

EventProcessorWithJsonOutput::completed_item_id328–332 ↗
fn completed_item_id(&mut self, raw_id: &str) -> String

作用:为“完成了”的原始条目取出之前开始时用过的输出编号。这样外部读者知道这是同一个任务结束了,而不是另起一个任务。

数据流:进去的是服务器原始条目的 raw_id → 它从映射表里删除并返回对应输出编号;如果没找到,说明之前没见过开始事件,就临时生成一个新编号 → 出来的是完成事件该用的编号,同时映射表被清理。

调用关系:map_completed_item_mut 会用它处理普通完成条目。它和 started_item_id 配合,像借书和还书登记一样,开始时登记,完成时销账。

EventProcessorWithJsonOutput::map_started_item334–342 ↗
fn map_started_item(&mut self, item: ThreadItem) -> Option<ExecThreadItem>

作用:把服务器的“条目开始”通知转换成可输出的“条目开始”内容。它会故意忽略助手消息和推理摘要的开始,因为这些通常只在完成时才有完整文字。

数据流:进去的是一个 ThreadItem → 如果是助手消息或推理摘要,就返回空;否则取它的原始编号,分配或复用输出编号,再调用 map_item_with_id 转成 ExecThreadItem → 出来的是可用于 ItemStarted 事件的条目,或空。

调用关系:collect_thread_events 收到 ServerNotification::ItemStarted 时调用它。它把具体条目翻译工作交给 map_item_with_id,把编号维护交给 started_item_id。

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

EventProcessorWithJsonOutput::map_completed_item_mut344–359 ↗
fn map_completed_item_mut(&mut self, item: ThreadItem) -> Option<ExecThreadItem>

作用:把服务器的“条目完成”通知转换成可输出的“条目完成”内容。它会过滤掉空白的推理摘要,避免输出没信息的噪声。

数据流:进去的是一个 ThreadItem → 如果是空推理摘要就丢弃;如果是助手消息或推理摘要,就生成一个新编号;其他条目则按原始编号找回开始时的输出编号 → 最后通过 map_item_with_id 转成输出条目。

调用关系:collect_thread_events 在收到 ItemCompleted 时调用它,也在补齐未完成条目时通过 reconcile_unfinished_started_items 间接使用它。它还帮助更新 final_message,因为完成的助手消息可能就是最终答案。

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

EventProcessorWithJsonOutput::reconcile_unfinished_started_items361–376 ↗
fn reconcile_unfinished_started_items(
        &mut self,
        turn_items: &[ThreadItem],
    ) -> Vec<ThreadEvent>

作用:在一轮结束时,把那些发过“开始”但没收到“完成”的条目补成完成事件。这样输出流不会留下半截任务。

数据流:进去的是这一轮里的所有 ThreadItem → 它检查哪些原始编号还留在“已开始未完成”映射表里 → 对这些条目调用 map_completed_item_mut,并包装成 ItemCompleted 事件 → 出来的是一批补齐用的完成事件,同时相关编号会被清掉。

调用关系:collect_thread_events 在 TurnCompleted 通知里调用它。它像关店前清账,确保外部界面不会一直显示某个命令或工具还在运行。

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

EventProcessorWithJsonOutput::final_message_from_turn_items378–392 ↗
fn final_message_from_turn_items(items: &[ThreadItem]) -> Option<String>

作用:从一轮对话的所有条目里找出最适合作为最终输出的文字。优先找最后一条助手消息,找不到才退而求其次用最后一个计划文本。

数据流:进去的是这一轮的条目列表 → 它从后往前查找 AgentMessage;如果没有,再从后往前查找 Plan → 出来的是找到的文本,或者空。

调用关系:回合成功完成时,collect_thread_events 会用它确定 final_message。这个 final_message 后面可能会被 print_final_output 写入 last_message 文件。

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

EventProcessorWithJsonOutput::thread_started_event394–398 ↗
fn thread_started_event(session_configured: &SessionConfiguredEvent) -> ThreadEvent

作用:根据会话配置生成“线程已开始”的事件。外部工具收到它,就知道一个新的对话线程正式建立了。

数据流:进去的是 SessionConfiguredEvent,里面有 thread_id → 它取出线程编号并包装成 ThreadStartedEvent → 出来的是 ThreadEvent::ThreadStarted。

调用关系:print_config_summary 在启动阶段会调用它,然后交给 emit 输出。它是 JSONL 输出流里最早出现的事件之一。

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

EventProcessorWithJsonOutput::collect_warning400–410 ↗
fn collect_warning(&mut self, message: String) -> CollectedThreadEvents

作用:把一条普通警告变成一个非致命的输出事件。它提醒外部用户有问题,但不会让整个流程停下。

数据流:进去的是警告文字 → 它生成一个新 item 编号,把文字包装成 ErrorItem,再放进 ItemCompleted 事件 → 出来的是 CollectedThreadEvents,状态仍然是 Running。

调用关系:process_warning 会直接用它;collect_thread_events 收到 Warning 通知时也会复用它。它只收集事件,不负责打印,打印由调用方交给 emit。

调用图:被 2 处调用(collect_thread_events, process_warning);外部调用 1 个(vec!)。

EventProcessorWithJsonOutput::collect_thread_events412–593 ↗
fn collect_thread_events(
        &mut self,
        notification: ServerNotification,
    ) -> CollectedThreadEvents

作用:这是本文件的主调度函数:接收一条服务器通知,判断它是哪类事情,再转换成零个、一个或多个 JSONL 线程事件。

数据流:进去的是 ServerNotification → 它按通知类型分支处理:警告变成错误样式条目,严重错误会记住,开始/完成条目会被映射,待办计划会开始或更新,token 用量会保存,回合完成会补齐未完成条目并决定是否准备关停 → 出来的是事件列表和当前运行状态,比如继续运行或发起关闭。

调用关系:process_server_notification 每收到后台通知就调用它。它会把细活交给 collect_warning、map_started_item、map_completed_item_mut、reconcile_unfinished_started_items、usage_from_last_total 等函数,自己负责把这些结果按流程串起来。

调用图:调用 6 个内部函数(collect_warning, map_completed_item_mut, map_started_item, next_item_id, reconcile_unfinished_started_items, usage_from_last_total);被 1 处调用(process_server_notification);外部调用 13 个(final_message_from_turn_items, map_todo_items, new, Error, ItemCompleted, ItemStarted, ItemUpdated, TurnCompleted, TurnFailed, TurnStarted (+3 more))。

EventProcessorWithJsonOutput::print_config_summary597–604 ↗
fn print_config_summary(
        &mut self,
        _: &Config,
        _: &str,
        session_configured: &SessionConfiguredEvent,
    )

作用:在会话配置完成后输出“线程开始”事件。虽然名字叫打印配置摘要,但这个 JSONL 版本主要是告诉外部程序线程已经启动。

数据流:进去的是配置、说明文字和会话配置事件;前两个参数在这里不用 → 它用 session_configured 生成 ThreadStarted 事件 → 通过 emit 打印成一行 JSON。

调用关系:这是实现 EventProcessor 接口的一部分,通常在启动阶段被上层运行器调用。它把 thread_started_event 的结果交给 emit,开启后续事件流。

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

EventProcessorWithJsonOutput::process_server_notification606–612 ↗
fn process_server_notification(&mut self, notification: ServerNotification) -> CodexStatus

作用:处理一条来自服务器的正式通知,并把产生的所有事件打印出去。它是运行过程中最常走的入口。

数据流:进去的是一条 ServerNotification → 它先调用 collect_thread_events 得到事件列表和状态 → 再逐个调用 emit 打印事件 → 出来的是 CodexStatus,告诉上层继续运行还是准备关闭。

调用关系:这是 EventProcessor 接口给主循环用的方法。主循环不断把服务器消息交给它;它内部依赖 collect_thread_events 做翻译,依赖 emit 做输出。

调用图:调用 2 个内部函数(collect_thread_events, emit)。

EventProcessorWithJsonOutput::process_warning614–620 ↗
fn process_warning(&mut self, message: String) -> CodexStatus

作用:处理运行器直接传来的警告,并把它作为 JSONL 事件打印出去。它用于那些不是标准服务器通知、但仍需要告诉外部用户的提醒。

数据流:进去的是警告文字 → 它调用 collect_warning 把文字变成事件和 Running 状态 → 再把事件逐个交给 emit 打印 → 出来的是继续运行的状态。

调用关系:这是 EventProcessor 接口的一部分。它和 collect_thread_events 里的 Warning 分支使用同一套 collect_warning 逻辑,保证警告输出格式一致。

调用图:调用 2 个内部函数(collect_warning, emit)。

EventProcessorWithJsonOutput::print_final_output622–628 ↗
fn print_final_output(&mut self)

作用:在程序收尾时,把最终助手消息写入指定的 last_message 文件。这样调用方除了读 JSONL,也能方便地拿到最后答案。

数据流:进去的是处理器当前状态,包括是否允许写最终消息、最终消息文本和保存路径 → 如果这一轮成功完成且配置了路径,就调用 handle_last_message 写文件;否则什么也不做 → 结果是磁盘上的最后消息文件可能被更新。

调用关系:这是关闭阶段的收尾动作。collect_thread_events 在回合成功时会打开 emit_final_message_on_shutdown,并保存 final_message;失败或中断时会关掉这个开关,防止 print_final_output 写出错误结果。

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

exec/src/event_processor.rs源码 ↗
orchestrationmain loop, teardown

这个文件像是给“事件处理员”写的一份岗位说明。程序运行时,后台代理会不断发来通知,比如配置已经准备好、产生了输出、需要警告用户等。这里的 EventProcessor trait(trait 可以理解成一份接口约定)规定:谁想当事件处理员,就必须会打印配置摘要、处理服务器通知、处理本地警告。CodexStatus 则像一个简单的红绿灯,告诉主流程是继续跑,还是准备关机。文件后半部分处理一个很实际的问题:运行结束时,用户可能希望把最后一条代理回复保存到指定文件。handle_last_message 会拿到这条回复,交给 write_last_message_file 写入磁盘;如果根本没有最后回复,它也会写一个空文件,并在终端提示用户。这能避免调用方以为文件没生成是程序坏了。

函数细节3
EventProcessor::print_final_output28–28 ↗
fn print_final_output(&mut self)

作用:这是事件处理员在流程结束时可以调用的“最后输出”步骤。默认实现什么也不做,意思是:不是每种输出方式都需要额外收尾。

数据流:进去的是当前事件处理员自己;默认情况下它不读取额外数据,也不打印、不写文件、不改变状态。出来时没有返回值,程序状态保持原样。

调用关系:它是 EventProcessor 这份接口的一部分,给具体实现留了一个收尾钩子。测试 failed_turn_does_not_overwrite_output_last_message_file 会触发它,用来确认失败场景下最后消息文件不会被不该写的内容覆盖;具体实现如果需要保存最终结果,通常会在这里继续调用 handle_last_message。

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

handle_last_message31–40 ↗
fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path)

作用:这个函数负责把“最后一条代理回复”保存到用户指定的文件里。即使没有最后回复,它也会写入空内容,并给出警告,避免用户找不到文件时一头雾水。

数据流:进去的是一个可能存在的最后回复和一个输出文件路径。它先把“没有回复”当成空字符串处理,然后调用 write_last_message_file 写到磁盘;如果原本确实没有回复,它还会往标准错误输出打印一条警告。出来时没有返回值,但磁盘上的目标文件会被写入或尝试写入。

调用关系:它通常在具体的 print_final_output 收尾流程里被调用,负责最后一步落盘。它自己不直接处理底层写文件错误,而是把写文件动作交给 write_last_message_file;如果发现没有消息,则自己负责告诉用户这件事。

调用图:调用 1 个内部函数(write_last_message_file);被 2 处调用(print_final_output, print_final_output);外部调用 1 个(eprintln!)。

write_last_message_file42–48 ↗
fn write_last_message_file(contents: &str, last_message_path: Option<&Path>)

作用:这个函数是真正动手写文件的小工具。它把给定文字写进指定路径;如果写失败,就把失败原因打印出来,而不是让整个程序崩掉。

数据流:进去的是要写入的文字,以及一个可选的文件路径。路径存在时,它调用系统的文件写入功能把内容写到磁盘;如果系统返回错误,比如路径不可写,它会把文件名和错误原因打印到标准错误输出。路径不存在时,它什么也不做。出来时没有返回值,主要影响是文件可能被写入,或者用户看到一条失败提示。

调用关系:它是 handle_last_message 背后的底层帮手,只关心“把这段文字写到这个地方”。handle_last_message 负责决定写什么、是否需要额外警告;write_last_message_file 负责实际写入,并在写入失败时报告问题。

调用图:被 1 处调用(handle_last_message);外部调用 2 个(eprintln!, write)。

ext/goal/src/accounting.rs源码 ↗
domain_logic会话回合开始、目标激活、工具完成后记录进度、目标暂停/完成/超预算时都会用到

这份代码像一个给目标计费的账本。一次对话回合开始时,它记下当时的 token 用量;目标变成活跃后,它继续看 token 增量和墙上时钟时间,也就是现实经过了多少秒。等外部把这些进度写到持久存储后,它再把“已入账”的基准往前挪,避免下次重复计算。文件里有互斥锁(一次只让一个线程改账本)保护内存状态,也有信号量(一张独占通行证)让并发的进度写入排队。它还特别处理 Plan 模式:这种规划回合不计 token。预算到顶的提示也只会对同一个目标报一次,避免反复吵用户。

函数细节34
GoalAccountingState::start_turn67–83 ↗
fn start_turn(
        &self,
        turn_id: impl Into<String>,
        collaboration_mode: ModeKind,
        token_usage_at_turn_start: &TokenUsage,
    )

作用:开始记录一个新的对话回合。它把回合编号放进账本,并用回合开始时的 token 用量作为起点,后面才能只算新增部分。

数据流:进去的是回合 ID、协作模式、回合开始时的 TokenUsage。函数先拿到账本锁,把当前回合设成这个 ID,再创建一份 GoalTurnAccounting;如果是 Plan 模式,就标成不计 token。出来没有返回值,但账本里多了这个回合的记录。

调用关系:它是每个回合的起点,会先通过 GoalAccountingState::inner 拿到账本,再交给 GoalTurnAccounting::new 建立单回合账本。后面的 record_token_usage、progress_snapshot 等函数都依赖这里留下的基准。

调用图:调用 2 个内部函数(inner, new);外部调用 4 个(clone, into, matches!, clone)。

GoalAccountingState::current_turn_id85–87 ↗
fn current_turn_id(&self) -> Option<String>

作用:查询现在正在进行的是哪个回合。外部流程需要知道当前回合 ID 时会用它。

数据流:进去不需要额外参数。函数读取内部账本里的 current_turn_id,复制一份返回;如果现在没有当前回合,就返回空。

调用关系:它只通过 GoalAccountingState::inner 读状态,不改状态。它常用于外层流程判断当前是否有可记账的回合。

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

GoalAccountingState::progress_accounting_permit94–98 ↗
async fn progress_accounting_permit(
        &self,
    ) -> Result<SemaphorePermit<'_>, tokio::sync::AcquireError>

作用:拿一张“进度记账通行证”。它保证同一条线程里的多个并发回调不会同时给同一段 token 或时间重复入账。

数据流:进去不需要业务数据。函数等待信号量发放唯一许可;成功后返回 SemaphorePermit,失败则返回获取错误。只要调用方持有这张许可,其他同类记账就得排队。

调用关系:它直接调用信号量的 acquire。调用方通常会在拍进度快照前拿到许可,等持久化写入成功并标记已入账后再释放许可。

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

GoalAccountingState::turn_is_current_active_goal100–109 ↗
fn turn_is_current_active_goal(&self, turn_id: &str) -> bool

作用:判断某个回合是不是当前回合,并且当前确实有一个正在计费的目标。它用于避免给过期回合或没有目标的回合记账。

数据流:进去的是一个 turn_id。函数读取账本,先确认它等于 current_turn_id,再找到对应回合,最后检查这个回合是否计 token 且有 active_goal_id;满足才返回 true。

调用关系:它通过 GoalAccountingState::inner 读状态,是外层流程做安全检查的小门卫。它不调用其他记账动作,也不改变账本。

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

GoalAccountingState::record_token_usage111–132 ↗
fn record_token_usage(
        &self,
        turn_id: impl Into<String>,
        total_usage: &TokenUsage,
    ) -> Option<RecordedTokenDelta>

作用:把某个回合当前看到的总 token 用量记进账本,并算出从上次入账以来新增了多少。它让系统知道是否有新的 token 需要写入目标进度。

数据流:进去的是回合 ID 和当前累计 TokenUsage。函数更新该回合的 current_token_usage;如果这个回合不计 token、找不到回合、或新增量不大于 0,就返回空。否则返回 RecordedTokenDelta,里面有本回合新增量和整个线程尚未刷新的新增量。

调用关系:它先通过 GoalAccountingState::inner 改状态,再依赖回合对象的差值计算能力。外层通常在收到模型或工具后的新用量时调用它,随后可能再走 progress_snapshot 和持久化写入。

调用图:调用 1 个内部函数(inner);外部调用 2 个(into, clone)。

GoalAccountingState::mark_turn_goal_active134–146 ↗
fn mark_turn_goal_active(&self, turn_id: &str, goal_id: impl Into<String>)

作用:把指定回合标记为某个目标正在活跃。这样后续 token 和时间才知道应该算到哪个目标头上。

数据流:进去的是 turn_id 和 goal_id。函数拿到账本锁,如果这个 goal_id 和之前已报告预算限制的目标不同,就清掉旧的预算报告标记;然后给对应回合写入 active_goal_id。如果这个回合正好是当前回合,也同步开启墙上时钟计时。

调用关系:它通过 GoalAccountingState::inner 操作账本,并把当前回合的时间计账交给 GoalWallClockAccounting::mark_active_goal。它常在外部确认某个回合关联到目标时使用。

调用图:调用 1 个内部函数(inner);外部调用 3 个(as_str, clone, into)。

GoalAccountingState::mark_current_turn_goal_active148–163 ↗
fn mark_current_turn_goal_active(
        &self,
        goal_id: impl Into<String>,
    ) -> Option<String>

作用:把“当前回合”标记为某个目标正在活跃,并从当前 token 用量重新开始算。这样目标刚激活前消耗的 token 不会误算进去。

数据流:进去的是 goal_id。函数读取当前回合 ID;没有当前回合或找不到回合就返回空。找到后写入 active_goal_id,把该回合的 token 基准重置为当前值,再启动墙上时钟,最后返回这个 turn_id。

调用关系:它通过 GoalAccountingState::inner 改状态,并调用 GoalTurnAccounting::reset_baseline_to_current 与 GoalWallClockAccounting::mark_active_goal。它是“现在这个目标开始计费了”的常用入口。

调用图:调用 1 个内部函数(inner);外部调用 3 个(as_str, clone, into)。

GoalAccountingState::mark_idle_goal_active165–172 ↗
fn mark_idle_goal_active(&self, goal_id: impl Into<String>)

作用:在没有具体 token 回合时,把某个目标标记为仍然在耗费时间。它主要用于只记时间、不记 token 的空闲阶段。

数据流:进去的是 goal_id。函数拿到账本锁,必要时清掉旧的预算报告标记,然后让墙上时钟开始为这个目标计时。它不改任何回合的 token 记录。

调用关系:它通过 GoalAccountingState::inner 进入账本,并把时间部分交给 GoalWallClockAccounting::mark_active_goal。后续 idle_progress_snapshot 会读取这里设置的活跃目标。

调用图:调用 1 个内部函数(inner);外部调用 2 个(as_str, into)。

GoalAccountingState::clear_current_turn_goal174–183 ↗
fn clear_current_turn_goal(&self) -> Option<String>

作用:清掉当前回合上的活跃目标。目标结束、暂停或不应继续计费时会用它。

数据流:进去不需要业务参数。函数找到 current_turn_id,把该回合的 active_goal_id 清空,同时停止墙上时钟的活跃目标,并清掉预算限制已报告标记;最后返回被清理的回合 ID,若没有当前回合则返回空。

调用关系:它通过 GoalAccountingState::inner 修改账本,并调用墙上时钟的清理能力。它比 clear_active_goal 更偏向“只清当前回合并告诉我清的是谁”。

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

GoalAccountingState::clear_active_goal185–194 ↗
fn clear_active_goal(&self)

作用:彻底清掉当前活跃目标,不管它是在当前回合上,还是只在时间计账里。它用于让账本回到“不再给任何目标计费”的状态。

数据流:进去没有参数。函数如果能找到当前回合,就把它的 active_goal_id 清空;然后清掉墙上时钟的活跃目标,并重置预算报告标记。没有返回值。

调用关系:它通过 GoalAccountingState::inner 拿到账本,并让 GoalWallClockAccounting::clear_active_goal 重置时间基准。外层在目标切换、取消或收尾时会调用它。

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

GoalAccountingState::progress_snapshot196–219 ↗
fn progress_snapshot(&self, turn_id: &str) -> Option<GoalProgressSnapshot>

作用:给某个回合拍一张“待入账进度快照”。快照里包含应该写给哪个目标、这次新增多少 token、经过多少秒。

数据流:进去的是 turn_id。函数读取该回合;若不计 token、没有活跃目标、或 token 和时间都没有新增,就返回空。否则返回 GoalProgressSnapshot,里面带当前 token 总量、目标 ID、时间增量和 token 增量。

调用关系:它通过 GoalAccountingState::inner 读当前账本,并使用回合 token 差值和墙上时钟时间差。调用方通常会先拿 progress_accounting_permit,再调用它,然后把快照写入持久存储。

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

GoalAccountingState::idle_progress_snapshot221–232 ↗
fn idle_progress_snapshot(&self) -> Option<IdleGoalProgressSnapshot>

作用:在空闲阶段拍一张只包含时间的进度快照。它用于目标没有新 token,但仍需要计算等待或执行时间的场景。

数据流:进去没有参数。函数读取墙上时钟里的活跃目标和距离上次入账的秒数;没有活跃目标或秒数为 0 就返回空。否则返回 IdleGoalProgressSnapshot。

调用关系:它通过 GoalAccountingState::inner 读取时间账本。它和 mark_idle_progress_accounted_for_status 配套使用:先拍快照,写入成功后再标记这段时间已入账。

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

GoalAccountingState::mark_progress_accounted_for_status234–256 ↗
fn mark_progress_accounted_for_status(
        &self,
        turn_id: &str,
        snapshot: &GoalProgressSnapshot,
        status: ThreadGoalStatus,
        budget_limited_goal_disposition: BudgetL

作用:告诉账本:刚才那张回合进度快照已经成功写入了。它会推进 token 和时间的基准,避免下次重复算同一段进度。

数据流:进去的是 turn_id、快照、目标当前状态,以及预算受限时是否清掉活跃目标的选择。函数先用 should_clear_active_goal 判断是否该停表;然后把该回合的 last_accounted_token_usage 更新成快照里的当前值,推进墙上时钟已入账时间,必要时清掉活跃目标和预算报告标记。

调用关系:它通过 GoalAccountingState::inner 改账本,并调用 should_clear_active_goal 做状态判断。它通常发生在 progress_snapshot 的结果已经成功保存之后。

调用图:调用 2 个内部函数(inner, should_clear_active_goal)。

GoalAccountingState::finish_turn258–264 ↗
fn finish_turn(&self, turn_id: &str)

作用:结束一个回合,并从账本里删除它。这样旧回合不会一直占内存,也不会被误当成还能记账。

数据流:进去的是 turn_id。函数拿到账本锁,删除 turns 里的这条回合记录;如果它正是 current_turn_id,就把当前回合也清空。没有返回值。

调用关系:它通过 GoalAccountingState::inner 修改状态。外层回合生命周期结束时会调用它,和 start_turn 首尾相接。

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

GoalAccountingState::mark_idle_progress_accounted_for_status266–281 ↗
fn mark_idle_progress_accounted_for_status(
        &self,
        snapshot: &IdleGoalProgressSnapshot,
        status: ThreadGoalStatus,
        budget_limited_goal_disposition: BudgetLimitedGoalDisp

作用:告诉账本:空闲阶段那段时间已经成功写入目标进度。它只处理时间,不处理 token。

数据流:进去的是 idle 快照、目标状态、以及预算受限时是否清掉活跃目标的选择。函数判断是否需要清目标,推进墙上时钟的已入账时间,必要时停止活跃目标,并在非预算限制状态下清掉预算报告标记。

调用关系:它通过 GoalAccountingState::inner 改时间账本,并调用 should_clear_active_goal。它和 idle_progress_snapshot 配套,是空闲计时写入成功后的收尾步骤。

调用图:调用 2 个内部函数(inner, should_clear_active_goal)。

GoalAccountingState::reset_idle_progress_baseline_and_clear_active_goal283–288 ↗
fn reset_idle_progress_baseline_and_clear_active_goal(&self)

作用:重置空闲计时的起点,并清掉活跃目标。它用于放弃当前空闲计时,避免旧时间被下一次误算。

数据流:进去没有参数。函数拿到账本锁,把墙上时钟基准重设为现在,清掉活跃目标,也清掉预算报告标记。没有返回值。

调用关系:它通过 GoalAccountingState::inner 直接操作墙上时钟。外层在空闲目标不再有效或需要重新开始计时时会用它。

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

GoalAccountingState::mark_budget_limit_reported_if_new290–297 ↗
fn mark_budget_limit_reported_if_new(&self, goal_id: &str) -> bool

作用:记录某个目标的“预算已到顶”提示是否已经报过。它防止同一个目标反复报告同一件事。

数据流:进去的是 goal_id。函数读取并比较内部的 budget_limit_reported_goal_id;如果已经是同一个目标,返回 false。否则写入这个 goal_id 并返回 true。

调用关系:它通过 GoalAccountingState::inner 修改小标记。外层在准备发预算限制提示前调用它,用返回值决定这次要不要真的提示。

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

GoalAccountingState::inner299–301 ↗
fn inner(&self) -> std::sync::MutexGuard<'_, GoalAccountingInner>

作用:安全地拿到内部账本。它把互斥锁的细节藏起来,让其他方法可以专心读写状态。

数据流:进去不需要参数。函数尝试锁住 Mutex;如果锁曾经因为其他线程崩溃而“中毒”,它也会取回里面的数据继续用。出来的是一把可读写内部账本的锁守卫。

调用关系:它被 clear_active_goal、clear_current_turn_goal、current_turn_id、finish_turn、idle_progress_snapshot、mark_budget_limit_reported_if_new、mark_current_turn_goal_active、mark_idle_goal_active 等大量方法调用,是所有状态读写的共同入口。

调用图:被 16 处调用(clear_active_goal, clear_current_turn_goal, current_turn_id, finish_turn, idle_progress_snapshot, mark_budget_limit_reported_if_new, mark_current_turn_goal_active, mark_idle_goal_active, mark_idle_progress_accounted_for_status, mark_progress_accounted_for_status (+6 more))。

GoalAccountingState::default305–310 ↗
fn default() -> Self

作用:创建一个全新的目标记账器。测试或新线程初始化时可以直接用它得到干净账本。

数据流:进去没有参数。函数创建默认的 GoalAccountingInner,并创建只有 1 个许可的 Semaphore。出来的是一个 GoalAccountingState,内部没有当前回合,也没有活跃目标。

调用关系:它调用 GoalAccountingInner::default 和外部库的 new。调用图里它被 goal_accounting_ignores_plan_mode_turns、goal_accounting_uses_turn_start_baseline_for_exact_deltas 等测试使用。

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

token_delta_since_last_accounting313–326 ↗
fn token_delta_since_last_accounting(last: &TokenUsage, current: &TokenUsage) -> i64

作用:计算两份累计 token 用量之间,应该算进目标账单的新增量。它先求差,再套用目标计费口径。

数据流:进去的是上次已入账的 TokenUsage 和当前 TokenUsage。函数逐项做安全减法,得到这段期间新增的输入、缓存输入、输出等 token;然后交给 goal_token_delta_for_usage 算最终应计数量。出来是一个整数增量。

调用关系:它是底层工具函数,被同名的回合方法 GoalTurnAccounting::token_delta_since_last_accounting 使用。它再调用 goal_token_delta_for_usage,把“怎样算目标 token”这个规则集中到一处。

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

goal_token_delta_for_usage328–333 ↗
fn goal_token_delta_for_usage(usage: &TokenUsage) -> i64

作用:定义“目标进度里 token 到底怎么算”。它把未缓存的输入 token 和输出 token 相加,缓存命中的输入不重复算。

数据流:进去的是一份 TokenUsage。函数用 input_tokens 减 cached_input_tokens,再加上非负的 output_tokens;如果输出 token 是负数,就按 0 处理。出来是应计入目标的 token 数。

调用关系:它被 token_delta_since_last_accounting 调用,是 token 计费口径的核心小规则。其他地方只要想保持同一算法,就应该走这条路。

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

GoalAccountingInner::default336–343 ↗
fn default() -> Self

作用:创建内部账本的初始内容。它让账本一开始没有当前回合、没有预算提示记录,但有一只已经准备好的墙上时钟。

数据流:进去没有参数。函数生成空的 current_turn_id、空的 turns 表、新的 GoalWallClockAccounting,以及空的 budget_limit_reported_goal_id。出来是 GoalAccountingInner。

调用关系:它被 GoalAccountingState::default 调用。它又调用 GoalWallClockAccounting::new,保证时间计账从创建那一刻开始有基准。

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

GoalAccountingInner::thread_unflushed_token_delta347–354 ↗
fn thread_unflushed_token_delta(&self) -> i64

作用:统计整条线程里还没写入持久存储的 token 新增量。它给外层一个总数,方便判断还有多少账没刷出去。

数据流:进去的是内部账本自身。函数遍历所有回合,只看需要计 token 的回合;每个回合取“当前值减已入账值”的正数部分,再安全相加。出来是总的未刷新增量。

调用关系:它由 record_token_usage 在返回 RecordedTokenDelta 时使用。它汇总多个 GoalTurnAccounting 的差值,让单次记录也能带上全线程视角。

GoalTurnAccounting::new358–365 ↗
fn new(current_token_usage: TokenUsage, account_tokens: bool) -> Self

作用:创建一个单回合的 token 账本。它把当前 token 用量同时作为“当前值”和“已入账基准”。

数据流:进去的是当前 TokenUsage 和是否计 token 的布尔值。函数复制 token 用量,设置 active_goal_id 为空,并保存 account_tokens 标记。出来是一份新的 GoalTurnAccounting。

调用关系:它被 GoalAccountingState::start_turn 调用。没有它,每个新回合就没有独立的 token 基准。

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

GoalTurnAccounting::active_goal_id367–369 ↗
fn active_goal_id(&self) -> Option<String>

作用:取出这个回合当前绑定的目标 ID。返回复制值,避免外部直接改内部状态。

数据流:进去的是这个回合对象。函数复制 active_goal_id 并返回;如果没有活跃目标,就返回空。

调用关系:它是回合对象的小读口,供进度快照这类流程确认“这段 token 应该算给谁”。

GoalTurnAccounting::reset_baseline_to_current371–373 ↗
fn reset_baseline_to_current(&mut self)

作用:把已入账基准重置为当前 token 用量。它常用于目标刚激活时,避免把激活之前的消耗算给新目标。

数据流:进去的是可修改的回合对象。函数把 current_token_usage 复制到 last_accounted_token_usage。出来没有返回值,但之后的 token 差值会从这个新基准开始算。

调用关系:它被 GoalAccountingState::mark_current_turn_goal_active 调用。它是目标切换或激活时校准 token 起点的关键动作。

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

GoalTurnAccounting::token_delta_since_last_accounting375–380 ↗
fn token_delta_since_last_accounting(&self) -> i64

作用:计算这个回合从上次入账到现在新增了多少可计 token。它把回合内部两份 TokenUsage 拿去比较。

数据流:进去的是这个回合对象。函数读取 last_accounted_token_usage 和 current_token_usage,交给文件级 token_delta_since_last_accounting。出来是一个整数 token 增量。

调用关系:它调用 token_delta_since_last_accounting。record_token_usage、progress_snapshot、thread_unflushed_token_delta 这类流程都依赖这个差值判断是否有新账。

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

GoalWallClockAccounting::new384–389 ↗
fn new() -> Self

作用:创建一个墙上时钟账本。墙上时钟指现实时间,不是 CPU 时间或 token 数。

数据流:进去没有参数。函数把 last_accounted_at 设为现在,active_goal_id 设为空。出来是一份新的 GoalWallClockAccounting。

调用关系:它被 GoalAccountingInner::default 调用。之后 mark_active_goal、time_delta_since_last_accounting 等方法都围绕这个起点工作。

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

GoalWallClockAccounting::time_delta_since_last_accounting391–393 ↗
fn time_delta_since_last_accounting(&self) -> i64

作用:计算距离上次时间入账已经过了多少秒。它告诉系统有多少现实时间还没算到目标上。

数据流:进去的是时间账本自身。函数用 last_accounted_at 到现在的 elapsed 秒数,转换成 i64;如果数值大到放不下,就用 i64 最大值。出来是秒数。

调用关系:它被 progress_snapshot 和 idle_progress_snapshot 间接用于生成时间增量。它只读时间,不改变基准。

调用图:外部调用 2 个(elapsed, try_from)。

GoalWallClockAccounting::mark_accounted395–404 ↗
fn mark_accounted(&mut self, accounted_seconds: i64)

作用:把一段秒数标记为已经入账。它不是简单设成现在,而是把基准向前推进指定秒数。

数据流:进去的是 accounted_seconds。小于等于 0 就什么也不做;否则把秒数转成 Duration,并尝试加到 last_accounted_at 上。如果时间加法溢出,就退回设为现在。出来没有返回值,但时间基准变新了。

调用关系:它被 mark_progress_accounted_for_status 和 mark_idle_progress_accounted_for_status 使用。这样写入成功多少秒,就只推进多少秒,未写入的时间不会丢。

调用图:外部调用 3 个(from_secs, checked_add, try_from)。

GoalWallClockAccounting::reset_baseline406–408 ↗
fn reset_baseline(&mut self)

作用:把时间基准重置为现在。它相当于重新按下秒表的开始键。

数据流:进去的是可修改的时间账本。函数把 last_accounted_at 改成 Instant::now。没有返回值。

调用关系:它被 GoalWallClockAccounting::mark_active_goal 和 GoalWallClockAccounting::clear_active_goal 调用。目标切换或清空时都要重置秒表,避免把旧目标的时间算给新目标。

调用图:被 2 处调用(clear_active_goal, mark_active_goal);外部调用 1 个(now)。

GoalWallClockAccounting::mark_active_goal410–416 ↗
fn mark_active_goal(&mut self, goal_id: impl Into<String>)

作用:让墙上时钟开始为某个目标计时。如果目标没变,就不重置;如果目标变了,就从现在重新开始。

数据流:进去的是 goal_id。函数比较它和当前 active_goal_id;如果不同,就先 reset_baseline,再保存新的目标 ID。没有返回值。

调用关系:它调用 GoalWallClockAccounting::reset_baseline。GoalAccountingState::mark_turn_goal_active、mark_current_turn_goal_active、mark_idle_goal_active 会把时间计账交给它。

调用图:调用 1 个内部函数(reset_baseline);外部调用 2 个(as_str, into)。

GoalWallClockAccounting::clear_active_goal418–421 ↗
fn clear_active_goal(&mut self)

作用:停止给任何目标计时,并把秒表重新归零到现在。它防止清掉目标后,空档时间被下一次误算。

数据流:进去的是可修改的时间账本。函数把 active_goal_id 设为空,再调用 reset_baseline。没有返回值。

调用关系:它调用 GoalWallClockAccounting::reset_baseline。GoalAccountingState 的清理方法和状态收尾流程会用它停止时间记账。

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

should_clear_active_goal424–439 ↗
fn should_clear_active_goal(
    status: ThreadGoalStatus,
    budget_limited_goal_disposition: BudgetLimitedGoalDisposition,
) -> bool

作用:根据目标状态判断是否应该清掉活跃目标。它把“哪些状态还能继续计费”这条规则集中写在一个地方。

数据流:进去的是 ThreadGoalStatus 和预算受限时的处理选择。状态是 Active 时返回 false;BudgetLimited 时看传入选择是保留还是清除;Paused、Blocked、UsageLimited、Complete 都返回 true。出来是一个布尔值。

调用关系:它被 GoalAccountingState::mark_progress_accounted_for_status 和 GoalAccountingState::mark_idle_progress_accounted_for_status 调用。两个收尾函数都靠它决定写入进度后要不要停掉当前目标。

调用图:被 2 处调用(mark_idle_progress_accounted_for_status, mark_progress_accounted_for_status);外部调用 1 个(matches!)。

git-utils/src/baseline.rs源码 ↗
domain_logicbaseline setup / diff scan

这个文件解决的是“我想知道这个目录从某个起点之后改了什么”的问题。它会在目录里重建或确认一个内部用的 .git,把当前文件写成一条没有父提交的基准提交。以后检查变化时,它不会直接依赖 git status 的文字输出,而是自己读取基准树和当前磁盘文件,比较每个文件的内容指纹和文件模式。文件模式就是普通文件、可执行文件、软链接这类 Git 关心的类型。最后它会产出两份结果:一份结构化清单,标明 A/M/D,也就是新增、修改、删除;另一份类似 Git diff 的文本补丁,方便人读或写入文件。要注意,重置函数会删除原来的 .git,所以它只适合内部工作目录,不能随便用在用户真正的仓库上。

函数细节36
GitBaselineChangeStatus::label30–36 ↗
fn label(self) -> &'static str

作用:把文件变化类型变成 Git 常见的短标签。比如新增变成 A,修改变成 M,删除变成 D,方便展示给人看。

数据流:进去的是一个变化状态 → 它按状态做一次简单匹配 → 出来的是一个很短的字符串标签,不改任何外部东西。

调用关系:这是变化结果的显示小帮手。别的流程先算出文件状态,需要用简短符号展示时,就可以调用它。

GitBaselineDiff::has_changes54–56 ↗
fn has_changes(&self) -> bool

作用:判断这次对比结果里到底有没有变化。调用者不用自己去数清单长度,只要问这一句就行。

数据流:进去的是一份 GitBaselineDiff 结果 → 它查看里面的变化清单是不是空的 → 出来一个真或假的答案,不修改结果本身。

调用关系:它被 render_workspace_diff_file 使用,用来决定是否需要继续展示或写出工作区差异。

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

reset_git_repository69–72 ↗
async fn reset_git_repository(root: &Path) -> anyhow::Result<()>

作用:异步地把某个目录重新变成一个干净的内部 Git 基准仓库。它会丢掉旧的 .git,然后把当前文件作为新的起点。

数据流:进去的是目录路径 → 它先复制路径,交给后台阻塞任务处理,避免卡住异步运行环境 → 出来是成功或失败;成功后目录里会有新的 .git 和一条基准提交。

调用关系:测试里的多个场景会调用它来建立起点。真正的重活交给 reset_git_repository_sync,外层只负责把同步磁盘工作放到 spawn_blocking 里运行。

调用图:被 6 处调用(diff_reports_added_modified_and_deleted_files, reports_executable_bit_changes_as_modified, reset_creates_fresh_baseline, reset_drops_previous_history, status_scan_does_not_write_added_file_blobs, write_index_ignores_configured_hooks_path);外部调用 2 个(to_path_buf, spawn_blocking)。

ensure_git_baseline_repository78–92 ↗
async fn ensure_git_baseline_repository(root: &Path) -> anyhow::Result<()>

作用:确保目录里有一个能用的内部 Git 基准仓库。已有仓库如果健康就保留,缺失或坏掉才重建。

数据流:进去的是目录路径 → 它创建目录,尝试打开 .git 并读取 HEAD 的文件列表 → 如果可用就直接成功;如果不可用,就重置成新基准仓库。

调用关系:测试 ensure_recovers_from_unborn_repository 会验证它能修复没有提交的仓库。它和 reset_git_repository 不同:它更温和,能用就不动。

调用图:被 1 处调用(ensure_recovers_from_unborn_repository);外部调用 2 个(to_path_buf, spawn_blocking)。

reset_git_repository_sync94–102 ↗
fn reset_git_repository_sync(root: &Path) -> anyhow::Result<()>

作用:同步版本的仓库重置流程,真正做删除旧 Git 元数据、初始化仓库、提交基准、写索引这些事。

数据流:进去的是目录路径 → 它创建目录,删除 .git,初始化新仓库,把当前文件写成提交,再把索引对齐到 HEAD → 出来是成功或错误;目录状态会被实际改变。

调用关系:它由异步外壳 reset_git_repository 间接调用,也会被 ensure_git_baseline_repository 在仓库不可用时调用。它把工作拆给 remove_git_metadatacommit_current_treewrite_index_from_head

调用图:调用 3 个内部函数(commit_current_tree, remove_git_metadata, write_index_from_head);外部调用 2 个(create_dir_all, init)。

diff_since_latest_init105–120 ↗
async fn diff_since_latest_init(root: &Path) -> anyhow::Result<GitBaselineDiff>

作用:算出从最近一次基准初始化到现在,目录里哪些文件变了,并生成一段标准风格的 diff 文本。

数据流:进去的是目录路径 → 它在后台打开仓库,读取基准提交里的文件表,扫描当前磁盘文件表,比较两边,再渲染文本差异 → 出来是一份 GitBaselineDiff,包含变化清单和 unified diff。

调用关系:多个测试用它验证新增、修改、删除、权限变化等结果。它是对外拿变化结果的主入口,内部串起 head_file_entriescurrent_file_entriesdiff_entriesrender_unified_diff

调用图:被 6 处调用(diff_reports_added_modified_and_deleted_files, ensure_recovers_from_unborn_repository, reports_executable_bit_changes_as_modified, reset_creates_fresh_baseline, reset_drops_previous_history, status_scan_does_not_write_added_file_blobs);外部调用 2 个(to_path_buf, spawn_blocking)。

remove_git_metadata122–135 ↗
fn remove_git_metadata(root: &Path) -> anyhow::Result<()>

作用:删除目录里的 .git 元数据,为重新建立内部基准仓库清场。没有 .git 时它什么也不做。

数据流:进去的是根目录 → 它查看 root/.git 是目录、文件还是不存在 → 如果存在就按类型删除;出来是成功或带上下文的错误。

调用关系:它只在 reset_git_repository_sync 的重置流程里使用。重置前必须先清掉旧 .git,否则新的基准可能混进旧历史。

调用图:被 1 处调用(reset_git_repository_sync);外部调用 4 个(join, remove_dir_all, remove_file, symlink_metadata)。

commit_current_tree137–155 ↗
fn commit_current_tree(repo: &gix::Repository, message: &str) -> anyhow::Result<()>

作用:把当前工作目录的文件内容写成 Git 的一条基准提交。这个提交就是以后做对比的“起跑线”。

数据流:进去的是 Git 仓库和提交说明 → 它找到工作目录,调用 write_tree 写出目录树,生成 Codex 作者签名,然后把树提交到 HEAD → 出来是成功或错误;仓库会多出一条基准提交。

调用关系:它由 reset_git_repository_sync 调用。它自己不逐个处理所有细节,而是把目录树构建交给 write_tree,把作者信息交给 codex_signature

调用图:调用 2 个内部函数(codex_signature, write_tree);被 1 处调用(reset_git_repository_sync);外部调用 4 个(commit_as, workdir, new, default)。

write_index_from_head157–160 ↗
fn write_index_from_head(root: &Path) -> anyhow::Result<()>

作用:把 Git 索引更新成和 HEAD 提交一致。索引可以理解成 Git 的“暂存清单”,这里要让它和基准完全对齐。

数据流:进去的是根目录 → 它运行 git read-tree --reset HEAD → 成功后 .git/index 反映基准提交里的文件;失败时返回错误。

调用关系:它由 reset_git_repository_sync 在基准提交后调用。测试 write_index_ignores_configured_hooks_path 还直接调用它,确认写索引时不会触发用户配置的钩子脚本。

调用图:调用 1 个内部函数(run_git_for_status);被 2 处调用(reset_git_repository_sync, write_index_ignores_configured_hooks_path)。

codex_signature162–171 ↗
fn codex_signature() -> gix::actor::Signature

作用:生成基准提交用的作者身份和当前时间。这样内部提交有固定名字和邮箱,来源清楚。

数据流:进去不需要参数 → 它读取当前 UTC 时间,填入 Codex 的名字、邮箱和时间 → 出来是一份 Git 提交签名。

调用关系:它被 commit_current_tree 调用。提交基准时需要作者和提交者信息,这个函数专门提供这份身份信息。

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

write_tree173–227 ↗
fn write_tree(repo: &gix::Repository, dir: &Path) -> anyhow::Result<ObjectId>

作用:把某个目录递归写成 Git 的 tree 对象。tree 可以理解成 Git 记录文件夹结构的清单。

数据流:进去的是仓库和目录路径 → 它遍历目录,跳过 .git,对子目录递归处理,对普通文件写 blob,对软链接写链接目标,再排序写成 tree → 出来是这个 tree 的对象编号。

调用关系:它由 commit_current_tree 调用,是创建基准提交的核心步骤。它会借助 file_mode 判断文件权限,借助 os_str_to_bstringpath_to_bytes 做跨系统路径转换。

调用图:调用 3 个内部函数(file_mode, os_str_to_bstring, path_to_bytes);被 1 处调用(commit_current_tree);外部调用 8 个(new, find_tree, write_blob, write_object, new, read, read_dir, read_link)。

head_file_entries229–237 ↗
fn head_file_entries(
    repo: &gix::Repository,
) -> anyhow::Result<BTreeMap<String, GitBaselineFileEntry>>

作用:读取基准提交里记录的全部文件。它把 Git 的树结构变成一张按路径查找的表。

数据流:进去的是仓库 → 它找到 HEAD 指向的树,从根开始递归收集文件路径、内容编号和模式 → 出来是一张路径到文件信息的有序表。

调用关系:它用于判断基准仓库是否可用,也用于后续 diff 对比。真正递归走树的工作交给 collect_tree_entries

调用图:调用 1 个内部函数(collect_tree_entries);外部调用 4 个(new, new, find_tree, head_tree_id)。

collect_tree_entries239–265 ↗
fn collect_tree_entries(
    repo: &gix::Repository,
    tree: gix::Tree<'_>,
    prefix: PathBuf,
    entries: &mut BTreeMap<String, GitBaselineFileEntry>,
) -> anyhow::Result<()>

作用:递归展开 Git tree,把里面每个非目录文件登记到结果表里。

数据流:进去的是仓库、当前 tree、路径前缀和待填写的表 → 它遍历 tree 条目,遇到子目录继续深入,遇到文件就保存斜杠格式路径、对象编号和模式 → 出来时表被补充完整。

调用关系:它被 head_file_entries 调用。它是“读取基准快照”的递归零件,路径转换会交给 bstr_to_pathpath_to_slash_string

调用图:调用 2 个内部函数(bstr_to_path, path_to_slash_string);被 1 处调用(head_file_entries);外部调用 3 个(join, find_tree, iter)。

current_file_entries267–274 ↗
fn current_file_entries(
    repo: &gix::Repository,
    root: &Path,
) -> anyhow::Result<BTreeMap<String, GitBaselineFileEntry>>

作用:扫描当前磁盘上的文件,做成一张和基准文件表同格式的表。这样两边才能直接比较。

数据流:进去的是仓库和根目录 → 它创建空表,然后递归扫描根目录下的当前文件 → 出来是一张路径到当前文件指纹和模式的表。

调用关系:它是当前状态扫描的入口。细节交给 collect_current_entries,自己主要负责准备结果容器。

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

collect_current_entries276–314 ↗
fn collect_current_entries(
    repo: &gix::Repository,
    root: &Path,
    dir: &Path,
    entries: &mut BTreeMap<String, GitBaselineFileEntry>,
) -> anyhow::Result<()>

作用:递归扫描当前目录,把普通文件和软链接记录下来,同时跳过内部 .git

数据流:进去的是仓库、根目录、当前正在扫的目录和结果表 → 它读取目录项,遇到子目录继续深入,遇到文件就计算 Git blob 哈希和文件模式,遇到软链接就记录链接目标 → 出来时结果表被填上当前文件状态。

调用关系:它被 current_file_entries 调用。它会用 blob_oid 只计算指纹而不写入 Git 对象库,用 relative_slash_path 生成相对路径,用 file_mode 记录可执行权限。

调用图:调用 4 个内部函数(blob_oid, file_mode, path_to_bytes, relative_slash_path);被 1 处调用(current_file_entries);外部调用 4 个(new, read, read_dir, read_link)。

blob_oid316–319 ↗
fn blob_oid(repo: &gix::Repository, bytes: &[u8]) -> anyhow::Result<ObjectId>

作用:根据一段文件内容算出 Git blob 对象编号。它只算指纹,不把内容真正写进仓库。

数据流:进去的是仓库和字节内容 → 它按仓库使用的哈希算法计算 blob 的对象 ID → 出来是这个内容在 Git 里的编号;仓库对象库不会被新增内容污染。

调用关系:它被 collect_current_entries 用来扫描当前文件,也被测试 status_scan_does_not_write_added_file_blobs 直接验证:扫描状态不能偷偷写入新对象。

调用图:被 2 处调用(collect_current_entries, status_scan_does_not_write_added_file_blobs);外部调用 2 个(object_hash, compute_hash)。

diff_entries321–349 ↗
fn diff_entries(
    head: &BTreeMap<String, GitBaselineFileEntry>,
    current: &BTreeMap<String, GitBaselineFileEntry>,
) -> Vec<GitBaselineChange>

作用:比较基准文件表和当前文件表,列出哪些路径新增、修改或删除。

数据流:进去的是两张按路径排列的文件表 → 它先看当前表里哪些是新增或内容/模式不同,再看基准表里哪些已经消失 → 出来是一份按路径排序的变化清单。

调用关系:它是 diff_since_latest_init 中判断变化类型的核心。它只产出结构化清单,不负责读文件内容,也不负责生成补丁文本。

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

render_unified_diff351–369 ↗
fn render_unified_diff(
    repo: &gix::Repository,
    root: &Path,
    head_entries: &BTreeMap<String, GitBaselineFileEntry>,
    current_entries: &BTreeMap<String, GitBaselineFileEntry>,
    change

作用:把一组文件变化渲染成统一 diff 文本。统一 diff 是常见的补丁格式,带有 +- 行。

数据流:进去的是仓库、根目录、基准表、当前表和变化清单 → 它逐个变化调用 render_change_diff,把每段文本拼起来 → 出来是一整段 diff 字符串。

调用关系:它由 diff_since_latest_init 在算出变化清单后使用。每个单文件的细节都交给 render_change_diff

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

render_change_diff371–431 ↗
fn render_change_diff(
    repo: &gix::Repository,
    root: &Path,
    head_entries: &BTreeMap<String, GitBaselineFileEntry>,
    current_entries: &BTreeMap<String, GitBaselineFileEntry>,
    change:

作用:为一个具体文件生成 diff 片段。它会处理新增、删除、修改,以及文件权限变化。

数据流:进去的是仓库、根目录、两边文件表和一个变化项 → 它读取旧内容和新内容,把字节按 UTF-8 宽松转成文本,补上文件头和模式信息,再生成带上下文的差异 → 出来是这个文件的一段 diff 文本。

调用关系:它被 render_unified_diff 逐个调用。它需要从基准侧读 blob,从当前侧读磁盘文件,并用 mode_label 把 Git 文件模式变成人能看懂的数字标签。

调用图:被 1 处调用(render_unified_diff);外部调用 4 个(from_utf8_lossy, new, from_lines, format!)。

read_head_blob433–436 ↗
fn read_head_blob(repo: &gix::Repository, entry: &GitBaselineFileEntry) -> anyhow::Result<Vec<u8>>

作用:从基准提交对应的 Git 对象里读出文件内容。也就是拿到“旧版本”的字节。

数据流:进去的是仓库和基准文件条目 → 它按对象编号找到 blob 并取出数据 → 出来是文件内容的字节数组。

调用关系:它在单文件 diff 渲染时使用。render_change_diff 需要旧内容来和当前内容做文本对比。

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

read_current_file_bytes438–449 ↗
fn read_current_file_bytes(root: &Path, relative_path: &str) -> anyhow::Result<Vec<u8>>

作用:读取当前磁盘上的文件内容;如果是软链接,就读取链接指向的路径文字,而不是打开目标文件。

数据流:进去的是根目录和相对路径 → 它拼出完整路径,查看文件类型,软链接读取链接目标,普通文件读取文件字节 → 出来是当前版本的字节内容。

调用关系:它服务于 render_change_diff。生成新旧对比时,旧内容来自 Git blob,新内容就靠这个函数从磁盘读取。

调用图:调用 1 个内部函数(path_to_bytes);外部调用 4 个(join, read, read_link, symlink_metadata)。

mode_label451–459 ↗
fn mode_label(mode: EntryMode) -> &'static str

作用:把 Git 内部的文件模式变成常见的数字字符串,比如普通文件 100644、可执行文件 100755

数据流:进去的是 Git 文件模式 → 它查看模式种类 → 出来是对应的固定数字标签。

调用关系:它在 diff 渲染时用于写 new file modedeleted file modeold mode/new mode 这些说明,让权限或类型变化能被看见。

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

file_mode474–476 ↗
fn file_mode(_path: &Path, default: EntryKind) -> anyhow::Result<EntryMode>

作用:判断一个磁盘文件在 Git 里应该用什么模式记录。Unix 系统上会特别识别可执行权限,非 Unix 系统则使用默认模式。

数据流:进去的是文件路径和默认文件种类 → 在 Unix 上它读取文件权限,看是否有执行位;有就标成可执行 blob,没有就用默认值;非 Unix 上直接返回默认值 → 出来是 Git 的 EntryMode

调用关系:它被 write_treecollect_current_entries 使用。这样基准提交和当前扫描都会用同一套规则记录文件权限。

调用图:被 2 处调用(collect_current_entries, write_tree);外部调用 2 个(into, metadata)。

os_str_to_bstring486–488 ↗
fn os_str_to_bstring(value: &OsStr) -> gix::bstr::BString

作用:把操作系统里的文件名转换成 Git tree 能存的字节字符串。不同系统对文件名的表示不完全一样,所以要集中处理。

数据流:进去的是一个系统文件名 → Unix 上直接取原始字节,非 Unix 上用可显示字符串转字节 → 出来是 Git 使用的 BString

调用关系:它被 write_tree 调用。写 Git tree 时,每个条目都需要一个 Git 可保存的文件名。

调用图:被 1 处调用(write_tree);外部调用 2 个(as_bytes, to_string_lossy)。

path_to_bytes498–500 ↗
fn path_to_bytes(path: &Path) -> Vec<u8>

作用:把路径转换成字节,主要用于记录软链接的目标。Git 对软链接保存的是链接目标文本,而不是目标文件内容。

数据流:进去的是路径 → Unix 上取原始路径字节,非 Unix 上取宽松字符串的字节 → 出来是一段字节数组。

调用关系:它被 write_treecollect_current_entriesread_current_file_bytes 使用。凡是需要把软链接目标当作内容保存或比较时,都会经过它。

调用图:被 3 处调用(collect_current_entries, read_current_file_bytes, write_tree);外部调用 2 个(as_os_str, to_string_lossy)。

bstr_to_path502–513 ↗
fn bstr_to_path(value: &gix::bstr::BStr) -> PathBuf

作用:把 Git tree 里存的字节文件名转回本机路径。这样程序才能继续拼接路径、递归读取子目录。

数据流:进去的是 Git 的字节字符串 → Unix 上按原始字节还原成系统字符串,非 Unix 上转成普通字符串路径 → 出来是 PathBuf

调用关系:它被 collect_tree_entries 使用。读取基准树时,Git 给的是字节名,递归收集路径前必须先转成本机路径。

调用图:被 1 处调用(collect_tree_entries);外部调用 3 个(to_string, from_bytes, from)。

relative_slash_path515–519 ↗
fn relative_slash_path(root: &Path, path: &Path) -> anyhow::Result<String>

作用:把一个完整文件路径变成相对根目录的路径,并统一用 / 分隔。这样结果不受操作系统路径分隔符影响。

数据流:进去的是根目录和某个文件路径 → 它先去掉根目录前缀,再转成斜杠格式字符串 → 出来是类似 dir/file.txt 的路径;失败时返回带上下文的错误。

调用关系:它被 collect_current_entries 用来给当前文件生成稳定的对比键。基准表和当前表都用这种斜杠路径,才能正确匹配。

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

path_to_slash_string521–526 ↗
fn path_to_slash_string(path: &Path) -> String

作用:把路径的各个部分用 / 拼起来,得到跨平台一致的路径字符串。

数据流:进去的是路径 → 它拆成路径组件,把每段转成可显示文字,再用 / 连接 → 出来是统一格式的字符串。

调用关系:它被 collect_tree_entries 使用,也间接服务于当前路径转换。统一路径格式是后面比较文件表的基础。

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

tests::git_stdout536–549 ↗
fn git_stdout(root: &Path, args: &[&str]) -> String

作用:测试用的小工具,用来在某个目录执行 git 命令并拿到标准输出。命令失败时会把输出打印出来,方便定位问题。

数据流:进去的是目录和 git 参数 → 它启动 git 子进程,检查退出状态,把 stdout 转成字符串 → 出来是命令输出;失败会让测试直接报错。

调用关系:多个测试用它检查真实 Git 看到的状态,比如 status --porcelainls-files。它只在测试代码里活跃。

调用图:外部调用 3 个(from_utf8_lossy, assert!, new)。

tests::reset_creates_fresh_baseline552–567 ↗
async fn reset_creates_fresh_baseline()

作用:测试重置函数能从一个普通目录创建出可用的基准仓库。它确认新建后没有未提交变化,索引也正常。

数据流:进去是测试自动创建的临时目录 → 它写入一个文件,调用 reset_git_repository,再调用 diff_since_latest_init 和真实 git 命令检查结果 → 测试通过表示基准仓库创建正确。

调用关系:它直接覆盖 reset_git_repositorydiff_since_latest_init 的基本路径,也通过 git_stdout 从外部 Git 视角验证结果。

调用图:调用 2 个内部函数(diff_since_latest_init, reset_git_repository);外部调用 5 个(new, assert!, assert_eq!, create_dir_all, write)。

tests::ensure_recovers_from_unborn_repository570–585 ↗
async fn ensure_recovers_from_unborn_repository()

作用:测试 ensure_git_baseline_repository 能修复“已经 init 但还没有提交”的仓库。这种仓库 HEAD 没有真正指向内容,不能当基准用。

数据流:进去是临时目录 → 它先写文件并只初始化 Git,不提交,然后调用 ensure 函数 → 出来通过断言确认已经生成基准,diff 为空,Git 索引正常。

调用关系:它专门验证 ensure_git_baseline_repository 的恢复分支,并用 diff_since_latest_initgit_stdout 检查修复后的状态。

调用图:调用 2 个内部函数(diff_since_latest_init, ensure_git_baseline_repository);外部调用 6 个(new, assert!, assert_eq!, create_dir_all, write, init)。

tests::write_index_ignores_configured_hooks_path589–630 ↗
async fn write_index_ignores_configured_hooks_path()

作用:测试写索引时不会执行用户配置的 Git hook。hook 可以理解成 Git 某些操作后自动跑的脚本,这里内部操作不应该触发它。

数据流:进去是 Unix 临时目录 → 它创建基准仓库,配置一个会写标记文件的 hook,再调用 write_index_from_head → 最后确认标记文件没有出现。

调用关系:它覆盖 write_index_from_head 的一个安全细节:内部重写索引应该可控,不能意外运行外部脚本。它先用 reset_git_repository 建好测试仓库。

调用图:调用 2 个内部函数(reset_git_repository, write_index_from_head);外部调用 8 个(new, assert!, format!, create_dir_all, metadata, set_permissions, write, git_stdout)。

tests::diff_reports_added_modified_and_deleted_files633–689 ↗
async fn diff_reports_added_modified_and_deleted_files()

作用:测试 diff 能同时报告新增、修改和删除三类文件,并且文本补丁里包含对应内容。

数据流:进去是临时目录 → 它先写基准文件并重置仓库,再修改一个文件、新增一个文件、删除一个文件,最后调用 diff → 断言变化清单和 diff 文本都符合预期。

调用关系:它覆盖 reset_git_repositorydiff_since_latest_init 的典型使用场景,是这个文件最核心行为的回归测试。

调用图:调用 2 个内部函数(diff_since_latest_init, reset_git_repository);外部调用 6 个(new, assert!, assert_eq!, create_dir_all, remove_file, write)。

tests::reset_drops_previous_history692–708 ↗
async fn reset_drops_previous_history()

作用:测试再次重置会丢掉旧历史,而不是在旧提交后面继续追加。这保证每次基准都是干净的新起点。

数据流:进去是临时目录 → 它先建一次基准,再改文件并再次重置,然后打开仓库检查 HEAD 提交没有父提交,且 diff 为空 → 测试通过表示旧历史确实被清掉。

调用关系:它验证 reset_git_repository 的破坏性语义:重置内部仓库时会删除旧 .git。随后用 diff_since_latest_init 确认新基准已生效。

调用图:调用 2 个内部函数(diff_since_latest_init, reset_git_repository);外部调用 6 个(new, assert!, assert_eq!, create_dir_all, write, open)。

tests::status_scan_does_not_write_added_file_blobs711–728 ↗
async fn status_scan_does_not_write_added_file_blobs()

作用:测试扫描当前变化时只计算哈希,不会把新增文件内容写进 Git 对象库。这样状态检查不会偷偷改变仓库内部存储。

数据流:进去是临时目录 → 它建立空基准,新增文件,调用 diff,再计算这个新增内容的 blob 编号并检查仓库里找不到该 blob → 测试通过表示扫描是只读的。

调用关系:它直接验证 diff_since_latest_initblob_oid 的重要性质。collect_current_entries 通过 blob_oid 算指纹,这个测试确保没有副作用。

调用图:调用 3 个内部函数(blob_oid, diff_since_latest_init, reset_git_repository);外部调用 5 个(new, assert!, create_dir_all, write, open)。

tests::reports_executable_bit_changes_as_modified732–755 ↗
async fn reports_executable_bit_changes_as_modified()

作用:测试 Unix 上文件可执行权限变化会被当作修改报告。即使内容没变,权限变了也对 Git 有意义。

数据流:进去是 Unix 临时目录 → 它写文件并建立基准,然后给文件加执行权限,再调用 diff → 出来通过断言确认变化为 Modified,diff 文本包含旧模式和新模式。

调用关系:它覆盖 file_mode 与 diff 渲染的配合。reset_git_repository 记录旧权限,diff_since_latest_init 扫描新权限并报告模式变化。

调用图:调用 2 个内部函数(diff_since_latest_init, reset_git_repository);外部调用 7 个(new, assert!, assert_eq!, create_dir_all, metadata, set_permissions, write)。

ollama/src/pull.rs源码 ↗
io_transport模型拉取下载期间

下载一个 Ollama 模型时,用户不能只看到程序卡住不动,所以这个文件像“下载进度播报员”。它先定义 PullEvent,也就是下载过程中会发生的几类消息:普通状态、某个数据层的字节进度、成功、错误。这里的 digest 可以理解成某个下载块的身份证。PullProgressReporter 是一个约定:谁想显示进度,就实现 on_event。CliProgressReporter 会把进度写到 stderr(命令行里的错误输出通道,常用来显示提示信息),并用同一行不断刷新下载量、百分比和速度。它会把多个下载块的进度加起来,算出总进度;还会跳过太吵的 “pulling manifest” 提示。错误事件这里不打印,避免外层再打印一次造成重复。TuiProgressReporter 目前只是借用命令行版显示方式,等以后再接真正的图形/终端界面。

函数细节4
CliProgressReporter::default39–41 ↗
fn default() -> Self

作用:这是给 CliProgressReporter 提供默认创建方式的函数。别人需要一个命令行进度播报员,但不想关心初始字段怎么填时,就可以用它。

数据流:进去没有额外输入 → 它直接调用 CliProgressReporter::new 来准备一个全新的进度播报员 → 出来一个初始状态干净的 CliProgressReporter。

调用关系:它是标准 Default 用法的一部分,自己不做复杂工作,只把创建任务交给 CliProgressReporter::new,保证默认创建和手动 new 创建出来的东西完全一致。

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

CliProgressReporter::new45–53 ↗
fn new() -> Self

作用:这个函数创建一个命令行进度播报员,并把所有计数器和记录表清零。它让后面的下载进度可以从 0 开始正确显示。

数据流:进去没有参数 → 它设置“还没打印标题”“上一行长度为 0”“已完成总量为 0”,记录当前时间,并准备一个空表来按 digest 保存每个下载块的总大小和已完成大小 → 出来一个可以接收下载事件的 CliProgressReporter。

调用关系:CliProgressReporter::default 会调用它来生成默认对象;外部的 ensure_oss_ready 也会在准备下载相关能力时调用它。它还用当前时间作为测速的起点,后面的 CliProgressReporter::on_event 会继续使用这个时间来算下载速度。

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

CliProgressReporter::on_event57–135 ↗
fn on_event(&mut self, event: &PullEvent) -> io::Result<()>

作用:这是命令行版进度显示的核心函数。每收到一个下载事件,它就决定要不要在终端上刷新文字、进度、速度,或者安静地忽略。

数据流:进去一个 PullEvent 下载事件,以及播报员自己保存的历史进度 → 如果是状态消息,它把文字写到 stderr 的同一行;如果是下载块进度,它更新对应 digest 的总量和完成量,再把所有块加总,算出 GB、百分比和 MB/s 速度并刷新显示;如果是错误,它什么也不打印;如果成功,它输出一个换行 → 出来是一个 io::Result,表示写终端是否成功,同时播报员内部会记住最新进度、上一行长度和测速时间。

调用关系:它是 PullProgressReporter 这个约定的命令行实现,下载流程每产生一个进度事件就会走到这里。它会使用 stderr 进行实际输出,用 format! 拼出人能看懂的文字,用当前时间计算速度。TuiProgressReporter::on_event 目前也把事件转交给它,所以命令行和 TUI 暂时看到的是同一套显示效果。

调用图:外部调用 3 个(format!, stderr, now)。

TuiProgressReporter::on_event144–146 ↗
fn on_event(&mut self, event: &PullEvent) -> io::Result<()>

作用:这是 TUI 版进度播报员的入口。TUI 指文字界面程序里更复杂的交互界面,但这里暂时还没有单独实现,所以先复用命令行显示。

数据流:进去一个 PullEvent 下载事件 → 它不自己解释事件,而是把事件交给内部包着的 CliProgressReporter::on_event → 出来同样是写输出是否成功的 io::Result,实际显示效果由命令行播报员完成。

调用关系:它实现同一个 PullProgressReporter 约定,让需要 TUI 播报员的地方也能接入下载流程。现阶段它只是一个转接头,把所有工作交给 CliProgressReporter::on_event,这样以后替换成真正 TUI 显示时,对外接口不用变。

TUI 状态与摘要界面

这些文件维护 TUI 面向状态的投射,包括页脚和页眉状态、目标和速率限制摘要、分支元数据,以及 /status 显示模型。

tui/src/chatwidget/status_surfaces.rs源码 ↗
domain_logiccross-cutting:界面刷新、任务状态变化、配置变化、Git 信息更新时活跃

聊天界面运行时,有很多状态会变:正在思考还是空闲、当前目录是哪、Git 分支是什么、剩余额度多少、是否需要用户批准。如果这些信息散落在界面各处,既难看又容易错。这个文件就像一个“告示牌排版员”:先读配置,知道用户想在状态栏和终端标题里显示哪些项目;再把无效配置挑出来,只提醒一次;然后补齐需要额外查询的信息,比如 Git 分支和 PR 信息;最后把能显示的内容拼成状态栏或终端标题。它还会避免重复写终端标题,减少无意义刷新;动画开启时,会安排下一帧,让标题里的小转圈或“需要操作”提示动起来。重要的是,状态栏和标题共用一份解析结果和缓存,避免同一轮刷新里重复做昂贵查询。

函数细节60
StatusSurfaceSelections::uses_git_branch54–59 ↗
fn uses_git_branch(&self) -> bool

作用:判断这次要显示的状态栏或终端标题里,是否有人需要 Git 分支名。这样程序才知道要不要去查分支,避免白做事。

数据流:输入是一份已解析好的显示项目清单 → 它检查状态栏项目和标题项目里有没有 GitBranch → 输出 true 或 false,不改动任何状态。

调用关系:它被 ChatWidget::sync_status_surface_shared_state 使用。刷新前先问它一句:这轮需要分支吗?需要才继续同步或发起分支查询。

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

StatusSurfaceSelections::uses_git_summary61–67 ↗
fn uses_git_summary(&self) -> bool

作用:判断这次显示内容里,是否需要更完整的 Git 摘要,比如 PR 编号或分支改动数。没有这些项目时,就不用查这类信息。

数据流:输入是本轮状态栏和标题的项目选择 → 它查看是否包含 PullRequestNumber 或 BranchChanges → 输出是否需要 Git 摘要,不产生副作用。

调用关系:它被 ChatWidget::sync_status_surface_shared_state 调用,用来决定是否保留、刷新或清掉 Git 摘要缓存。

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

ChatWidget::status_surface_selections82–92 ↗
fn status_surface_selections(&self) -> StatusSurfaceSelections

作用:把状态栏和终端标题的配置一次性读出来,并分成“能识别的项目”和“写错的项目”。这是后面整轮刷新共用的清单。

数据流:输入来自 ChatWidget 当前配置 → 它调用状态栏和标题各自的解析函数 → 输出 StatusSurfaceSelections,里面装着有效项目和无效项目列表。

调用关系:它是刷新流程的第一步。ChatWidget::refresh_status_surfaces、ChatWidget::refresh_terminal_title,以及手动请求 Git 刷新时都会先调用它。

调用图:调用 2 个内部函数(status_line_items_with_invalids, terminal_title_items_with_invalids);被 4 处调用(refresh_status_surfaces, refresh_terminal_title, request_status_line_branch_refresh, request_status_line_git_summary_refresh)。

ChatWidget::warn_invalid_status_line_items_once94–113 ↗
fn warn_invalid_status_line_items_once(&mut self, invalid_items: &[String])

作用:如果用户在状态栏配置里写了不认识的项目名,这个函数负责提醒一次。只提醒一次是为了不刷屏。

数据流:输入是无效项目名列表,以及组件内部是否已经警告过的标记 → 它在会话已开始且还没提醒过时拼出提示文字 → 调用警告入口显示消息,并把“已提醒”标记设上。

调用关系:它由 ChatWidget::refresh_status_surfaces 调用,发生在真正渲染状态栏之前,让用户知道哪些配置被忽略了。

调用图:被 1 处调用(refresh_status_surfaces);外部调用 1 个(format!)。

ChatWidget::warn_invalid_terminal_title_items_once115–134 ↗
fn warn_invalid_terminal_title_items_once(&mut self, invalid_items: &[String])

作用:如果用户在终端标题配置里写了不认识的项目名,这个函数负责提醒一次。它避免同一个错误在每次标题刷新时反复出现。

数据流:输入是无效标题项目列表和内部警告标记 → 它检查是否已有会话、是否有错误、是否第一次警告 → 生成一条“已忽略这些项目”的提示并显示。

调用关系:ChatWidget::refresh_status_surfaces 和 ChatWidget::refresh_terminal_title 都会调用它,因为标题可能单独刷新。

调用图:被 2 处调用(refresh_status_surfaces, refresh_terminal_title);外部调用 1 个(format!)。

ChatWidget::sync_status_surface_shared_state136–160 ↗
fn sync_status_surface_shared_state(&mut self, selections: &StatusSurfaceSelections)

作用:同步状态栏和标题共用的后台信息,主要是 Git 分支和 Git 摘要。它保证需要时才查,不需要时就清掉旧数据,避免显示过期信息。

数据流:输入是本轮选择清单 → 它判断是否需要 Git 分支和摘要,必要时取得当前目录、同步缓存键、发起异步查询 → 更新内部缓存状态和 pending 标记。

调用关系:它位于解析配置之后、渲染之前。ChatWidget::refresh_status_surfaces 和 ChatWidget::refresh_terminal_title 都会用它,为后续显示函数准备共享数据。

调用图:调用 7 个内部函数(request_status_line_branch, request_status_line_git_summary, status_line_cwd, sync_status_line_branch_state, sync_status_line_git_summary_state, uses_git_branch, uses_git_summary);被 2 处调用(refresh_status_surfaces, refresh_terminal_title)。

ChatWidget::refresh_status_line_from_selections162–188 ↗
fn refresh_status_line_from_selections(&mut self, selections: &StatusSurfaceSelections)

作用:根据本轮选择真正重画底部状态栏。它会把每个能显示的项目变成文字,再拼成一条状态栏。

数据流:输入是已解析的状态栏项目 → 如果没有项目就关闭状态栏并清掉链接;否则逐项取显示值,交给 status_line_from_segments 排版 → 更新底部状态栏文字和可能的 PR 超链接。

调用关系:它由 ChatWidget::refresh_status_surfaces 调用,是完整刷新流程里负责“底部那一行”的环节。

调用图:调用 1 个内部函数(status_line_value_for_item);被 1 处调用(refresh_status_surfaces);外部调用 2 个(new, status_line_from_segments)。

ChatWidget::clear_managed_terminal_title195–202 ↗
fn clear_managed_terminal_title(&mut self) -> std::io::Result<()>

作用:清掉 Codex 之前写到终端窗口标题上的内容。它不会恢复用户原来的标题,只负责把自己管过的标题撤掉。

数据流:输入是内部记录的 last_terminal_title → 如果之前确实写过标题,就向终端发送清除标题的控制命令 → 成功后把缓存设为空,返回是否成功。

调用关系:它被 ChatWidget::refresh_terminal_title_from_selections 调用。当标题配置为空、生成不出内容,或终端认为没有可见内容时,会走到这里。

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

ChatWidget::refresh_terminal_title_from_selections211–254 ↗
fn refresh_terminal_title_from_selections(&mut self, selections: &StatusSurfaceSelections)

作用:根据本轮选择生成并写入终端窗口标题。它还负责避免重复写标题,并在需要动画时安排下一次刷新。

数据流:输入是终端标题项目清单和当前运行状态 → 它判断是否需要“用户操作”提示,生成标题文字,和上次标题比较 → 必要时写入或清空终端标题,并可能安排下一帧动画。

调用关系:它由 ChatWidget::refresh_status_surfaces 和 ChatWidget::refresh_terminal_title 调用,是“终端标题”这条显示路径的核心出口。

调用图:调用 4 个内部函数(clear_managed_terminal_title, terminal_title_animation_interval_with_selections, terminal_title_shows_action_required_with_selections, terminal_title_text_for_selections);被 2 处调用(refresh_status_surfaces, refresh_terminal_title);外部调用 2 个(now, debug!)。

ChatWidget::refresh_status_surfaces262–269 ↗
fn refresh_status_surfaces(&mut self)

作用:一次性刷新底部状态栏和终端标题。它是这个文件里最完整的刷新入口。

数据流:没有额外输入,读取当前配置和运行状态 → 解析选择、提示无效配置、同步共享状态、刷新状态栏、刷新标题 → 更新界面和终端标题相关缓存。

调用关系:当界面需要整体更新状态信息时会调用它。它把本文件里的几个步骤串成一条流水线。

调用图:调用 6 个内部函数(refresh_status_line_from_selections, refresh_terminal_title_from_selections, status_surface_selections, sync_status_surface_shared_state, warn_invalid_status_line_items_once, warn_invalid_terminal_title_items_once)。

ChatWidget::refresh_terminal_title272–277 ↗
fn refresh_terminal_title(&mut self)

作用:只刷新终端窗口标题,不碰底部状态栏。适合标题动画或标题相关状态变化时使用。

数据流:读取当前配置和运行状态 → 解析标题选择,提示无效标题项目,同步必要共享状态 → 调用标题渲染函数写入或清理标题。

调用关系:它是较轻量的刷新入口,复用了 ChatWidget::status_surface_selections、ChatWidget::sync_status_surface_shared_state 和 ChatWidget::refresh_terminal_title_from_selections。

调用图:调用 4 个内部函数(refresh_terminal_title_from_selections, status_surface_selections, sync_status_surface_shared_state, warn_invalid_terminal_title_items_once)。

ChatWidget::terminal_title_requires_action279–281 ↗
fn terminal_title_requires_action(&self) -> bool

作用:询问底部面板:现在是不是有事情卡住了,需要用户操作。比如等待确认或输入。

数据流:输入是 ChatWidget 当前底部面板状态 → 它直接读取底部面板的判断结果 → 输出 true 或 false。

调用关系:ChatWidget::terminal_title_shows_action_required 和 ChatWidget::terminal_title_shows_action_required_with_selections 会调用它,决定标题里是否显示“需要操作”。

调用图:被 2 处调用(terminal_title_shows_action_required, terminal_title_shows_action_required_with_selections)。

ChatWidget::terminal_title_shows_action_required283–285 ↗
fn terminal_title_shows_action_required(&self) -> bool

作用:判断终端标题是否应该显示“需要操作”的提醒。只有真的需要用户操作,并且标题配置里启用了活动提示,才会显示。

数据流:输入是当前底部面板状态和标题配置 → 它组合“是否需要操作”和“标题是否使用 activity/spinner”两个条件 → 输出是否展示提醒。

调用关系:它被动画判断和活动进度判断使用,用来避免普通转圈动画和“需要操作”提醒互相打架。

调用图:调用 2 个内部函数(terminal_title_requires_action, terminal_title_uses_activity);被 2 处调用(should_animate_terminal_title_action_required, terminal_title_has_active_progress)。

ChatWidget::terminal_title_text_for_selections287–312 ↗
fn terminal_title_text_for_selections(
        &mut self,
        selections: &StatusSurfaceSelections,
        now: Instant,
    ) -> Option<String>

作用:把终端标题各个项目拼成最终标题文字。需要用户操作时,它会改走专门的醒目标题格式。

数据流:输入是标题项目清单和当前时间 → 如果需要操作,生成提醒标题;否则逐项取值,按项目间隔符拼接 → 输出标题字符串,或在没有任何可显示内容时输出 None。

调用关系:它被 ChatWidget::refresh_terminal_title_from_selections 调用,是写入终端前生成标题文本的地方。

调用图:调用 2 个内部函数(action_required_terminal_title_text, terminal_title_shows_action_required_with_selections);被 1 处调用(refresh_terminal_title_from_selections);外部调用 1 个(new)。

ChatWidget::action_required_terminal_title_text314–325 ↗
fn action_required_terminal_title_text(
        &mut self,
        selections: &StatusSurfaceSelections,
        now: Instant,
    ) -> String

作用:生成“需要用户操作”时的特殊终端标题。它会把醒目前缀和其他标题片段组合起来。

数据流:输入是本轮标题选择和当前时间 → 它先得到当前闪烁阶段的前缀,再把除状态项外的可用片段拼进去 → 输出一整条提醒标题。

调用关系:它只在 ChatWidget::terminal_title_text_for_selections 判断需要用户操作时被调用,并把排版细节交给 bottom_pane 的构造函数。

调用图:调用 1 个内部函数(action_required_terminal_title_prefix_at);被 1 处调用(terminal_title_text_for_selections);外部调用 1 个(build_action_required_title_text)。

ChatWidget::action_required_terminal_title_prefix_at327–339 ↗
fn action_required_terminal_title_prefix_at(&self, now: Instant) -> &'static str

作用:决定“需要操作”标题当前该显示哪个前缀。动画开着时,它会在明显和不明显两种前缀之间切换,形成闪烁效果。

数据流:输入是当前时间和动画起点 → 如果动画关闭,固定返回醒目前缀;否则按经过时间算奇偶阶段 → 返回当前阶段的前缀文本。

调用关系:它被 ChatWidget::action_required_terminal_title_text 调用,专门给提醒标题提供开头那一段。

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

ChatWidget::terminal_title_shows_action_required_with_selections341–349 ↗
fn terminal_title_shows_action_required_with_selections(
        &self,
        selections: &StatusSurfaceSelections,
    ) -> bool

作用:用本轮解析后的标题项目判断是否显示“需要操作”。它比直接看配置更准确,因为已经过滤了无效项目。

数据流:输入是本轮标题选择和当前是否需要用户操作 → 它检查标题项目里是否包含 Spinner → 输出是否进入 action required 显示模式。

调用关系:标题渲染、标题文本生成和动画间隔判断都会调用它,保证同一轮刷新里的判断一致。

调用图:调用 1 个内部函数(terminal_title_requires_action);被 3 处调用(refresh_terminal_title_from_selections, terminal_title_animation_interval_with_selections, terminal_title_text_for_selections)。

ChatWidget::terminal_title_animation_interval_with_selections351–363 ↗
fn terminal_title_animation_interval_with_selections(
        &self,
        selections: &StatusSurfaceSelections,
    ) -> Option<Duration>

作用:决定终端标题下一次动画刷新要等多久。不同动画有不同节奏:需要操作提示慢一些,转圈动画快一些。

数据流:输入是标题选择、动画开关和运行状态 → 先判断是否是“需要操作”闪烁;否则判断是否要转圈 → 输出一个时间间隔,或 None 表示不用动画。

调用关系:它被 ChatWidget::refresh_terminal_title_from_selections 调用,用来给 frame_requester 安排下一帧。

调用图:调用 2 个内部函数(should_animate_terminal_title_spinner_with_selections, terminal_title_shows_action_required_with_selections);被 1 处调用(refresh_terminal_title_from_selections)。

ChatWidget::request_status_line_branch_refresh365–373 ↗
fn request_status_line_branch_refresh(&mut self)

作用:主动请求刷新 Git 分支名。只有当前状态栏或标题真的要显示分支时,它才会发起查询。

数据流:读取当前显示选择和当前目录 → 如果没人需要分支就直接返回;否则同步分支缓存状态并启动分支查询 → 更新 pending 状态,稍后通过事件收到结果。

调用关系:它是外部触发分支刷新时用的入口,内部复用 ChatWidget::status_surface_selections、ChatWidget::sync_status_line_branch_state 和 ChatWidget::request_status_line_branch。

调用图:调用 4 个内部函数(request_status_line_branch, status_line_cwd, status_surface_selections, sync_status_line_branch_state)。

ChatWidget::request_status_line_git_summary_refresh375–383 ↗
fn request_status_line_git_summary_refresh(&mut self)

作用:主动请求刷新 Git 摘要,比如 PR 和改动统计。只有界面确实要显示这些内容时才查。

数据流:读取当前选择和当前目录 → 不需要摘要就返回;需要时同步摘要缓存键并发起异步摘要查询 → 后续通过事件把结果送回界面。

调用关系:它是外部触发 Git 摘要刷新时用的入口,和分支刷新入口结构类似。

调用图:调用 4 个内部函数(request_status_line_git_summary, status_line_cwd, status_surface_selections, sync_status_line_git_summary_state)。

ChatWidget::status_line_items_with_invalids388–390 ↗
fn status_line_items_with_invalids(&self) -> (Vec<StatusLineItem>, Vec<String>)

作用:把状态栏配置里的字符串项目名转成程序认识的状态栏项目,同时收集写错的名字。

数据流:读取配置中的状态栏项目列表 → 调用通用解析函数 parse_items_with_invalids → 输出有效项目列表和无效字符串列表。

调用关系:它被 ChatWidget::status_surface_selections 调用,是整轮刷新最早的配置解析步骤之一。

调用图:调用 2 个内部函数(configured_status_line_items, parse_items_with_invalids);被 1 处调用(status_surface_selections)。

ChatWidget::configured_status_line_items392–399 ↗
fn configured_status_line_items(&self) -> Vec<String>

作用:拿到用户配置的状态栏项目;如果用户没配,就返回默认状态栏项目。这样界面总有合理默认值。

数据流:读取 config.tui_status_line → 有配置就复制出来;没有配置就把默认项目转成字符串列表 → 输出项目名列表。

调用关系:它被 ChatWidget::status_line_items_with_invalids 调用,为后续解析提供原始字符串。

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

ChatWidget::terminal_title_items_with_invalids404–406 ↗
fn terminal_title_items_with_invalids(&self) -> (Vec<TerminalTitleItem>, Vec<String>)

作用:把终端标题配置里的字符串项目名转成程序认识的标题项目,同时记下不认识的名字。

数据流:读取配置中的标题项目列表 → 调用 parse_items_with_invalids 做转换和去重收集错误 → 输出有效标题项目和无效项目名。

调用关系:它被 ChatWidget::status_surface_selections 调用,和状态栏解析一起组成本轮显示快照。

调用图:调用 2 个内部函数(configured_terminal_title_items, parse_items_with_invalids);被 1 处调用(status_surface_selections)。

ChatWidget::configured_terminal_title_items409–416 ↗
fn configured_terminal_title_items(&self) -> Vec<String>

作用:拿到用户配置的终端标题项目;如果没配,就使用默认顺序。默认标题很克制,只放活动状态和项目名。

数据流:读取 config.tui_terminal_title → 有值就复制;没有就返回 DEFAULT_TERMINAL_TITLE_ITEMS → 输出字符串项目列表。

调用关系:它被 ChatWidget::terminal_title_items_with_invalids 调用,为标题项目解析提供输入。

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

ChatWidget::status_line_cwd418–422 ↗
fn status_line_cwd(&self) -> &Path

作用:找出状态栏和标题应该以哪个目录为“当前目录”。如果聊天过程中有当前工作目录,就用它;否则用配置里的启动目录。

数据流:读取 current_cwd 和 config.cwd → 优先返回 current_cwd,否则返回 config.cwd → 输出一个目录路径引用,不修改状态。

调用关系:很多显示值都依赖它,比如当前目录、项目名、Git 查询和标题里的目录显示。

调用图:被 6 处调用(request_status_line_branch_refresh, request_status_line_git_summary_refresh, status_line_project_root_name, status_line_value_for_item, sync_status_surface_shared_state, terminal_title_value_for_item)。

ChatWidget::status_line_project_root_for_cwd429–447 ↗
fn status_line_project_root_for_cwd(&self, cwd: &Path) -> Option<PathBuf>

作用:从一个当前目录推断项目根目录。优先用 Git 仓库根目录,找不到 Git 时再从项目配置层里找。

数据流:输入一个目录路径 → 先尝试向上找 Git 仓库根;失败后查看配置层里的项目 .codex 文件夹 → 输出项目根目录路径,或 None。

调用关系:它被 ChatWidget::status_line_project_root_name_for_cwd 调用,后者再把路径变成适合显示的项目名。

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

ChatWidget::status_line_project_root_name_for_cwd449–455 ↗
fn status_line_project_root_name_for_cwd(&self, cwd: &Path) -> Option<String>

作用:把某个目录对应的项目根目录变成一个好看的名字。一般就是项目文件夹名。

数据流:输入当前目录 → 调用项目根目录查找函数 → 如果找到根目录,取最后一级文件夹名;取不到就用格式化路径 → 输出可显示的项目名。

调用关系:它被 ChatWidget::status_line_project_root_name 调用,是带缓存的项目名获取流程里的实际计算步骤。

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

ChatWidget::status_line_project_root_name458–472 ↗
fn status_line_project_root_name(&mut self) -> Option<String>

作用:获取当前项目名,并缓存结果。终端标题刷新很频繁,缓存能避免每次都去文件系统里重新找项目根。

数据流:读取当前目录和项目名缓存 → 如果缓存目录相同,直接返回缓存名;否则重新计算项目名并更新缓存 → 输出项目名或 None。

调用关系:它被状态栏项目 ProjectRoot 和终端标题项目 Project 使用,是项目名显示的共享入口。

调用图:调用 2 个内部函数(status_line_cwd, status_line_project_root_name_for_cwd);被 2 处调用(status_line_value_for_item, terminal_title_project_name)。

ChatWidget::terminal_title_project_name478–490 ↗
fn terminal_title_project_name(&mut self) -> Option<String>

作用:生成终端标题里的项目名。它优先用项目根目录名,找不到时退回当前目录名,并限制长度。

数据流:读取缓存项目名或当前目录名 → 得到一个候选名字 → 用 truncate_terminal_title_part 截短到适合标题的长度 → 输出标题片段。

调用关系:它被 ChatWidget::terminal_title_value_for_item 和 ChatWidget::status_surface_preview_value_for_item 调用,用于真实标题和预览。

调用图:调用 1 个内部函数(status_line_project_root_name);被 2 处调用(status_surface_preview_value_for_item, terminal_title_value_for_item);外部调用 1 个(truncate_terminal_title_part)。

ChatWidget::sync_status_line_branch_state496–508 ↗
fn sync_status_line_branch_state(&mut self, cwd: &Path)

作用:当当前目录变了时,重置 Git 分支缓存。因为分支是按目录查的,目录变了还显示旧分支就会误导用户。

数据流:输入新的当前目录 → 如果和缓存目录一样就不动;否则记录新目录,并清空分支值、pending 状态和完成标记 → 更新内部缓存状态。

调用关系:它被共享状态同步和手动分支刷新调用,通常紧接着可能会发起新的分支查询。

调用图:被 2 处调用(request_status_line_branch_refresh, sync_status_surface_shared_state);外部调用 1 个(to_path_buf)。

ChatWidget::sync_status_line_git_summary_state510–518 ↗
fn sync_status_line_git_summary_state(&mut self, cwd: &Path)

作用:当当前目录变了时,重置 Git 摘要缓存。这样 PR 编号和改动统计不会从上一个项目串到当前项目。

数据流:输入新的当前目录 → 如果目录没变就返回;如果变了就保存新目录,清空摘要、pending 状态和完成标记 → 更新内部状态。

调用关系:它被共享状态同步和手动 Git 摘要刷新调用,之后可能调用 ChatWidget::request_status_line_git_summary 去查新数据。

调用图:被 2 处调用(request_status_line_git_summary_refresh, sync_status_surface_shared_state);外部调用 1 个(to_path_buf)。

ChatWidget::request_status_line_branch524–538 ↗
fn request_status_line_branch(&mut self, cwd: PathBuf)

作用:启动一次异步 Git 分支查询。异步的意思是把慢活放到后台做,不堵住界面。

数据流:输入要查询的目录 → 如果已有查询在跑就不重复启动;如果没有命令运行器就标记完成;否则设置 pending,并在后台查当前分支 → 查询结果通过 AppEvent 发回主界面。

调用关系:它由共享状态同步和手动刷新入口调用,实际查询交给 branch_summary::current_branch_name。

调用图:调用 1 个内部函数(current_branch_name);被 2 处调用(request_status_line_branch_refresh, sync_status_surface_shared_state);外部调用 1 个(spawn)。

ChatWidget::request_status_line_git_summary540–554 ↗
fn request_status_line_git_summary(&mut self, cwd: PathBuf)

作用:启动一次异步 Git 摘要查询,用来拿 PR 信息和分支改动统计。这样状态栏不用等 Git 命令跑完才响应。

数据流:输入要查询的目录 → 如果已有查询在跑就跳过;没有运行器就标记完成;否则后台计算摘要 → 结果作为 AppEvent 发回。

调用关系:它由共享状态同步和手动摘要刷新入口调用,后台工作交给 branch_summary::status_line_git_summary。

调用图:调用 1 个内部函数(status_line_git_summary);被 2 处调用(request_status_line_git_summary_refresh, sync_status_surface_shared_state);外部调用 1 个(spawn)。

ChatWidget::status_line_value_for_item561–658 ↗
fn status_line_value_for_item(&mut self, item: StatusLineItem) -> Option<String>

作用:把一个状态栏项目变成实际要显示的文字。比如模型名、当前目录、权限、用量、Git 分支、任务进度等。

数据流:输入一个 StatusLineItem → 它根据项目类型读取配置、运行状态、缓存、用量和限制信息 → 输出一段文字;如果当前还没有可用数据,就输出 None 表示暂时不显示。

调用关系:它是状态栏取值核心,被状态栏刷新、预览和部分终端标题项目复用。

调用图:调用 8 个内部函数(model_with_reasoning_display_name, reasoning_display_name, run_state_status_text, status_line_cwd, status_line_project_root_name, terminal_title_task_progress, approval_mode_display, permissions_display);被 3 处调用(refresh_status_line_from_selections, status_surface_preview_value_for_item, terminal_title_value_for_item);外部调用 2 个(limit_label_for_window, format!)。

ChatWidget::status_line_pull_request_url660–665 ↗
fn status_line_pull_request_url(&self) -> Option<String>

作用:取出当前 Git 摘要里的 PR 链接。这样状态栏显示 PR 编号时,可以顺便带上可点击链接。

数据流:读取 status_line_git_summary → 如果里面有 pull_request,就复制其 url → 输出链接字符串或 None。

调用关系:它服务于状态栏渲染中 PR 编号的超链接设置,依赖 Git 摘要查询先完成。

ChatWidget::status_surface_preview_value_for_item667–701 ↗
fn status_surface_preview_value_for_item(
        &mut self,
        item: StatusSurfacePreviewItem,
    ) -> Option<String>

作用:给状态栏/标题配置预览生成示例值或当前值。用户调配置时,可以看到某个项目大概会显示什么。

数据流:输入一个预览项目枚举 → 有些项目直接返回固定值或标题专用值,其余映射成状态栏项目再取值 → 输出预览文字或 None。

调用关系:它复用 ChatWidget::status_line_value_for_item、ChatWidget::terminal_title_project_name 等函数,让预览和真实显示尽量一致。

调用图:调用 4 个内部函数(run_state_status_text, status_line_value_for_item, terminal_title_project_name, terminal_title_task_progress)。

ChatWidget::terminal_title_value_for_item706–770 ↗
fn terminal_title_value_for_item(
        &mut self,
        item: TerminalTitleItem,
        now: Instant,
    ) -> Option<String>

作用:把一个终端标题项目变成标题里的一个片段。它会注意长度,避免标题太长难看。

数据流:输入标题项目和当前时间 → 根据项目类型取应用名、项目名、转圈动画、状态、线程名、Git 分支、用量等 → 对很多片段做长度截断,输出文字或 None。

调用关系:它被标题文本生成过程使用,也复用状态栏取值函数来保持两处信息一致。

调用图:调用 8 个内部函数(model_with_reasoning_display_name, reasoning_display_name, run_state_status_text, status_line_cwd, status_line_value_for_item, terminal_title_project_name, terminal_title_spinner_text_at, terminal_title_task_progress);外部调用 1 个(truncate_terminal_title_part)。

ChatWidget::reasoning_display_name772–775 ↗
fn reasoning_display_name(&self) -> String

作用:生成当前推理强度的显示名称。推理强度可以理解为模型“想得多深”的档位。

数据流:读取当前有效 reasoning effort → 交给标签格式化函数 → 输出一段适合状态栏和标题显示的文字。

调用关系:它被模型加推理显示、状态栏取值和终端标题取值调用。

调用图:被 3 处调用(model_with_reasoning_display_name, status_line_value_for_item, terminal_title_value_for_item);外部调用 1 个(status_line_reasoning_effort_label)。

ChatWidget::model_with_reasoning_display_name777–791 ↗
fn model_with_reasoning_display_name(&self) -> String

作用:生成“模型名 + 推理档位 + 可能的服务档位”的完整显示名。用户能一眼看出当前用的是什么模型配置。

数据流:读取模型名、推理标签、当前服务档位和账号状态 → 如果能匹配到服务档位名称就追加进去 → 输出组合后的模型描述文字。

调用关系:它被状态栏和终端标题的模型相关项目调用,内部会先调用 ChatWidget::reasoning_display_name。

调用图:调用 1 个内部函数(reasoning_display_name);被 2 处调用(status_line_value_for_item, terminal_title_value_for_item);外部调用 1 个(format!)。

ChatWidget::run_state_status_text797–818 ↗
fn run_state_status_text(&self) -> String

作用:把当前运行状态压缩成简单词,比如 Starting、Ready、Working、Waiting、Thinking。它专门用于状态栏和标题里的短状态。

数据流:读取 MCP 启动状态、终端标题状态种类、底部面板是否有任务在跑 → 启动中优先显示 Starting;没有任务时显示 Ready;否则按状态显示 → 输出短文本。

调用关系:它被状态栏取值、预览取值和终端标题取值调用,是多个显示面共享的状态文字来源。

调用图:被 3 处调用(status_line_value_for_item, status_surface_preview_value_for_item, terminal_title_value_for_item)。

ChatWidget::terminal_title_spinner_text_at820–830 ↗
fn terminal_title_spinner_text_at(&self, now: Instant) -> Option<String>

作用:生成终端标题里的转圈动画字符。只有动画开启且确实有活在进行时才显示。

数据流:输入当前时间 → 先检查动画开关和是否有活动进度 → 如果满足条件就按时间取一个 spinner 帧 → 输出字符字符串或 None。

调用关系:它被 ChatWidget::terminal_title_value_for_item 调用,为 Spinner 这个标题项目提供内容。

调用图:调用 2 个内部函数(terminal_title_has_active_progress, terminal_title_spinner_frame_at);被 1 处调用(terminal_title_value_for_item)。

ChatWidget::terminal_title_spinner_frame_at832–837 ↗
fn terminal_title_spinner_frame_at(&self, now: Instant) -> &'static str

作用:根据当前时间算出转圈动画该显示哪一帧。它像翻小人书一样按固定间隔换字符。

数据流:输入当前时间和动画起点 → 计算经过了多少个 100 毫秒间隔 → 用余数从固定帧数组里选一帧 → 输出一个静态字符片段。

调用关系:它只被 ChatWidget::terminal_title_spinner_text_at 调用,是 spinner 文本的底层计时器。

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

ChatWidget::terminal_title_uses_activity839–845 ↗
fn terminal_title_uses_activity(&self) -> bool

作用:判断终端标题配置是否启用了活动提示。默认配置也算启用,因为默认标题包含 activity。

数据流:读取 tui_terminal_title 配置 → 没配置时返回 true;有配置时检查是否包含 activity 或 spinner → 输出布尔值。

调用关系:它被 ChatWidget::terminal_title_shows_action_required 和 ChatWidget::should_animate_terminal_title_spinner 调用,用来决定标题是否参与活动显示。

调用图:被 2 处调用(should_animate_terminal_title_spinner, terminal_title_shows_action_required)。

ChatWidget::terminal_title_has_active_progress847–853 ↗
fn terminal_title_has_active_progress(&self) -> bool

作用:判断现在是否有值得在标题里显示动画的活动。比如启动中或任务正在跑。

数据流:读取是否正在显示“需要操作”、MCP 启动状态和底部任务状态 → 如果需要操作则不算普通活动;否则启动中或任务运行中返回 true → 输出布尔值。

调用关系:它被 spinner 文本和动画判断函数调用,防止空闲时标题还一直转。

调用图:调用 1 个内部函数(terminal_title_shows_action_required);被 3 处调用(should_animate_terminal_title_spinner, should_animate_terminal_title_spinner_with_selections, terminal_title_spinner_text_at)。

ChatWidget::should_animate_terminal_title_spinner855–859 ↗
fn should_animate_terminal_title_spinner(&self) -> bool

作用:判断终端标题的小转圈是否应该继续动画。它综合动画开关、标题配置和当前是否有活动。

数据流:读取 config.animations、标题是否使用活动项、是否有活动进度 → 三个条件都满足才输出 true → 不直接改状态。

调用关系:这是外部或其他模块可用的判断入口,内部调用 ChatWidget::terminal_title_uses_activity 和 ChatWidget::terminal_title_has_active_progress。

调用图:调用 2 个内部函数(terminal_title_has_active_progress, terminal_title_uses_activity)。

ChatWidget::should_animate_terminal_title_action_required861–863 ↗
fn should_animate_terminal_title_action_required(&self) -> bool

作用:判断“需要用户操作”的标题提醒是否应该闪烁。只有动画开关打开且标题确实显示该提醒时才需要。

数据流:读取动画配置和当前 action required 显示判断 → 输出是否应安排闪烁动画。

调用关系:它调用 ChatWidget::terminal_title_shows_action_required,服务于标题动画调度相关逻辑。

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

ChatWidget::should_animate_terminal_title_spinner_with_selections865–874 ↗
fn should_animate_terminal_title_spinner_with_selections(
        &self,
        selections: &StatusSurfaceSelections,
    ) -> bool

作用:用本轮解析后的标题选择判断 spinner 是否该动。这样不会被无效配置或旧配置影响。

数据流:输入本轮选择 → 检查动画是否开启、选择里是否有 Spinner、当前是否有活动进度 → 输出是否需要 spinner 动画。

调用关系:它被 ChatWidget::terminal_title_animation_interval_with_selections 调用,用来决定下一帧的刷新间隔。

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

ChatWidget::terminal_title_task_progress877–883 ↗
fn terminal_title_task_progress(&self) -> Option<String>

作用:把最近一次任务计划进度变成标题或状态栏里的短文字,比如 Tasks 2/5。用户能快速知道任务完成到哪一步。

数据流:读取 transcript.last_plan_progress → 如果没有进度或总数为 0,返回 None;否则格式化完成数和总数 → 输出进度文字。

调用关系:它被状态栏取值、标题取值和预览取值调用,是任务进度显示的统一来源。

调用图:被 3 处调用(status_line_value_for_item, status_surface_preview_value_for_item, terminal_title_value_for_item);外部调用 1 个(format!)。

ChatWidget::truncate_terminal_title_part886–900 ↗
fn truncate_terminal_title_part(value: String, max_chars: usize) -> String

作用:把终端标题中的单个片段截短,太长时加省略号。它按“用户看见的字符”截,避免把表情或组合字符切坏。

数据流:输入原文字和最大字符数 → 按 grapheme cluster(一个用户可见字符单位)取前面部分 → 如果超长且空间允许,就用 ... 结尾 → 输出截短后的字符串。

调用关系:很多标题取值函数会调用它,保证项目名、目录、线程名等不会把终端标题撑得太长。

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

five_hour_status_window903–910 ↗
fn five_hour_status_window(
    snapshot: &RateLimitSnapshotDisplay,
) -> Option<(&RateLimitWindowDisplay, bool)>

作用:从限额快照里挑出适合显示为“五小时限制”的那个窗口。不同账号返回的窗口结构可能不同,所以这里做兼容选择。

数据流:输入一个限额快照 → 按优先级寻找标签为 5h 的主窗口或其他可替代的非周限制窗口 → 输出窗口引用和它是否是次级窗口。

调用关系:它在状态栏显示 FiveHourLimit 时被使用,内部首先调用 find_primary_codex_window,后续也会按源码里的兜底规则继续找。

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

weekly_status_window912–917 ↗
fn weekly_status_window(
    snapshot: &RateLimitSnapshotDisplay,
) -> Option<(&RateLimitWindowDisplay, bool)>

作用:从限额快照里挑出适合显示为“周限制”的窗口。找不到明确 weekly 时,会用次级窗口兜底。

数据流:输入限额快照 → 先查找标签为 weekly 的窗口;如果没有,再看 secondary 是否存在 → 输出窗口和是否为次级窗口。

调用关系:它在状态栏显示 WeeklyLimit 时使用,并调用 find_codex_window 完成按标签查找。

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

find_codex_window919–936 ↗
fn find_codex_window(
    snapshot: &'a RateLimitSnapshotDisplay,
    label: &str,
) -> Option<(&'a RateLimitWindowDisplay, bool)>

作用:在主窗口和次级窗口里按标签找一个限额窗口。标签比如 5h 或 weekly。

数据流:输入限额快照和目标标签 → 先检查 primary,再检查 secondary,每次用 matches_window_label 判断 → 输出匹配窗口和是否为 secondary。

调用关系:它被 weekly_status_window 和 secondary_window_with_label_when_weekly_is_available 使用,是通用的限额窗口查找器。

调用图:调用 1 个内部函数(matches_window_label);被 2 处调用(secondary_window_with_label_when_weekly_is_available, weekly_status_window)。

find_primary_codex_window938–948 ↗
fn find_primary_codex_window(
    snapshot: &'a RateLimitSnapshotDisplay,
    label: &str,
) -> Option<(&'a RateLimitWindowDisplay, bool)>

作用:只在主限额窗口里按标签查找。适合那些必须优先认主窗口的场景。

数据流:输入限额快照和目标标签 → 取 primary,没有就返回 None;有则检查标签是否匹配 → 输出主窗口引用或 None。

调用关系:它被 five_hour_status_window 调用,作为五小时限制选择的第一优先级。

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

secondary_window_with_label_when_weekly_is_available950–962 ↗
fn secondary_window_with_label_when_weekly_is_available(
    snapshot: &'a RateLimitSnapshotDisplay,
    label: &str,
) -> Option<(&'a RateLimitWindowDisplay, bool)>

作用:在确认有周限制窗口存在时,再尝试找带指定标签的次级窗口。这是为了处理某些限额数据排列比较特殊的情况。

数据流:输入限额快照和标签 → 先确认能找到 weekly;再检查 secondary 是否存在且标签匹配 → 输出次级窗口或 None。

调用关系:它是 five_hour_status_window 兜底链条中的一环,内部调用 find_codex_window 和 matches_window_label。

调用图:调用 2 个内部函数(find_codex_window, matches_window_label)。

non_weekly_primary_window964–973 ↗
fn non_weekly_primary_window(
    snapshot: &RateLimitSnapshotDisplay,
) -> Option<(&RateLimitWindowDisplay, bool)>

作用:返回不是周限制的主窗口。它用于在找不到明确 5h 标签时,拿一个看起来像短期限制的主窗口兜底。

数据流:输入限额快照 → 取 primary;如果它是 weekly 就不要,否则返回它 → 输出窗口和 false。

调用关系:它属于 five_hour_status_window 的兜底选择逻辑,靠 matches_window_label 判断是不是 weekly。

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

non_weekly_secondary_window_when_primary_is_weekly975–989 ↗
fn non_weekly_secondary_window_when_primary_is_weekly(
    snapshot: &RateLimitSnapshotDisplay,
) -> Option<(&RateLimitWindowDisplay, bool)>

作用:当主窗口是周限制时,尝试把非周限制的次级窗口当作短期限额来显示。

数据流:输入限额快照 → 先确认 primary 是 weekly;再检查 secondary 存在且不是 weekly → 输出 secondary 和 true,否则 None。

调用关系:它也是 five_hour_status_window 的兜底逻辑之一,使用 matches_window_label 做标签判断。

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

matches_window_label991–997 ↗
fn matches_window_label(window: &RateLimitWindowDisplay, label: &str) -> bool

作用:判断某个限额窗口的时长标签是不是目标标签,比如 5h 或 weekly。

数据流:输入一个限额窗口和目标标签 → 把窗口分钟数转换成人类可读的持续时间标签 → 和目标标签比较 → 输出 true 或 false。

调用关系:多个限额窗口查找函数都会调用它。它把“分钟数怎么对应标签”的细节集中在一处。

调用图:被 5 处调用(find_codex_window, find_primary_codex_window, non_weekly_primary_window, non_weekly_secondary_window_when_primary_is_weekly, secondary_window_with_label_when_weekly_is_available)。

permissions_display999–1026 ↗
fn permissions_display(config: &Config) -> String

作用:把复杂的权限配置压缩成状态栏能看懂的短标签,比如 Read Only、Workspace、Full Access 或 Custom permissions。

数据流:输入整体配置 → 先看是否有用户命名的权限档;否则计算实际权限范围和工作区根目录,再生成摘要 → 根据摘要归类成短文字。

调用关系:它被 ChatWidget::status_line_value_for_item 调用,用于 Permissions 状态栏项目。

调用图:被 1 处调用(status_line_value_for_item);外部调用 2 个(effective_workspace_roots, summarize_permission_profile)。

approval_mode_display1028–1038 ↗
fn approval_mode_display(config: &Config) -> String

作用:把审批策略变成用户能读懂的显示文字。审批策略就是程序做敏感操作前要不要问用户。

数据流:输入配置 → 把内部审批策略转换成协议里的 AskForApproval 类型 → 如果是按请求审批,再区分自动审和人工审;否则直接显示策略值 → 输出文字。

调用关系:它被 ChatWidget::status_line_value_for_item 调用,用于 ApprovalMode 状态栏项目。

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

parse_items_with_invalids1040–1058 ↗
fn parse_items_with_invalids(ids: impl IntoIterator<Item = String>) -> (Vec<T>, Vec<String>)

作用:通用地解析配置项目名:认识的转成枚举,不认识的收集起来。它还能让重复错误只出现一次。

数据流:输入一串字符串 ID → 逐个尝试 parse;成功就放进有效列表,失败就按出现顺序去重后加入无效列表 → 输出有效项目和带引号的无效项目名。

调用关系:它被状态栏项目解析和终端标题项目解析共同使用,避免两套配置解析逻辑重复写。

调用图:被 2 处调用(status_line_items_with_invalids, terminal_title_items_with_invalids);外部调用 3 个(new, new, format!)。

tui/src/branch_summary.rs源码 ↗
domain_logicstatus line background refresh

这个文件像状态栏背后的“侦察员”。它会在当前工作目录里悄悄运行 git 和 gh(GitHub 命令行工具),看看现在在哪个分支、有没有打开的拉取请求(PR,也就是准备合并代码的申请)、这个分支比默认分支多加或删除了多少行。它刻意不直接启动系统命令,而是通过 WorkspaceCommandExecutor 这层“代跑命令的人”来执行,所以不管 TUI 连的是本地服务还是远程服务,同一套逻辑都能用。它还有一个重要原则:所有查询都是尽力而为。比如没装 git、没登录 gh、目录不是 Git 仓库、仓库状态奇怪,都只会返回空信息,而不是弹错误。这样状态栏能显示多少算多少,界面不会因为后台探测失败而坏掉。

函数细节28
current_branch_name100–112 ↗
async fn current_branch_name(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<String>

作用:查询当前目录检出的 Git 分支名,用来给状态栏显示“我现在在哪个分支”。如果不是 Git 仓库、处于 detached HEAD(没有分支名的临时状态)或命令失败,就安静地返回没有结果。

数据流:输入是一个命令执行器和工作目录路径。它通过 run_git_command 执行 git branch --show-current,检查命令是否成功,再把输出前后空白去掉;如果结果不是空字符串,就输出分支名,否则输出 None,不改动仓库。

调用关系:状态栏请求分支名时会用到它,比如 request_status_line_branch 和 on_agent_message_item_completed。它自己只负责问 Git,具体跑命令的细节交给 run_git_command。

调用图:调用 1 个内部函数(run_git_command);被 2 处调用(request_status_line_branch, on_agent_message_item_completed)。

status_line_git_summary119–131 ↗
async fn status_line_git_summary(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> StatusLineGitSummary

作用:一次性收集状态栏需要的 Git 摘要:打开的 PR 和本分支改动行数。它把两个互不依赖的查询并行做,减少等待时间。

数据流:输入是命令执行器和工作目录。它同时启动 open_pull_request 和 branch_diff_stats_to_default_branch;两个结果回来后,组装成 StatusLineGitSummary。任何一边查不到都可以为空,另一边仍然保留。

调用关系:request_status_line_git_summary 会调用它来刷新状态栏缓存。它是这个文件里把 PR 查询和分支差异查询串起来的总入口。

调用图:被 1 处调用(request_status_line_git_summary);外部调用 1 个(join!)。

branch_diff_stats_to_default_branch138–191 ↗
async fn branch_diff_stats_to_default_branch(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<GitBranchDiffStats>

作用:计算当前分支已经提交的代码,相对仓库默认分支增加了多少行、删除了多少行。它不看未提交的本地改动,因为状态栏这里想表达的是“这个分支本身”的变化。

数据流:输入是命令执行器和目录。它先确认这是 Git 仓库,再找到默认分支引用,接着用 git merge-base 找当前 HEAD 和默认分支共同的起点,然后执行 git diff --numstat 起点..HEAD。最后逐行累加新增列和删除列,输出 GitBranchDiffStats;任何一步失败就输出 None。

调用关系:status_line_git_summary 会把它作为分支改动统计的来源。它依赖 get_default_branch 找比较对象,依赖 run_git_command 跑 Git 命令;测试 tests::branch_diff_stats_prefers_remote_default_ref_over_stale_local_branch 会验证它优先用远程默认分支。

调用图:调用 2 个内部函数(get_default_branch, run_git_command);被 1 处调用(branch_diff_stats_prefers_remote_default_ref_over_stale_local_branch);外部调用 1 个(format!)。

get_git_remotes198–210 ↗
async fn get_git_remotes(runner: &dyn WorkspaceCommandExecutor, cwd: &Path) -> Option<Vec<String>>

作用:读取当前仓库配置了哪些远程仓库,并把 origin 放到最前面。origin 通常是主远程仓库,所以优先试它更符合常见用法。

数据流:输入是命令执行器和目录。它运行 git remote,把输出的每一行当成一个远程仓库名;如果里面有 origin,就挪到列表第一位。成功时输出远程名列表,失败时输出 None。

调用关系:get_default_branch 会先调用它,按这个顺序尝试从各个远程仓库找默认分支。它把实际 Git 调用交给 run_git_command。

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

get_default_branch217–236 ↗
async fn get_default_branch(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<DefaultBranch>

作用:找出应该拿哪个分支当“默认分支”来比较当前分支。它尽量使用远程跟踪分支,避免本地 main 太旧或根本不存在导致统计不准。

数据流:输入是命令执行器和目录。它先拿到远程仓库列表,然后对每个远程仓库依次尝试 symbolic-ref 方法和 remote show 方法;都失败后,再退回本地 main 或 master。输出 DefaultBranch,里面保存可用于 merge-base 的 Git 引用;找不到就输出 None。

调用关系:branch_diff_stats_to_default_branch 需要它来决定和谁比较。它把不同发现方式分派给 get_remote_default_branch_from_symbolic_ref、get_remote_default_branch_from_remote_show 和 get_default_branch_local。

调用图:调用 4 个内部函数(get_default_branch_local, get_git_remotes, get_remote_default_branch_from_remote_show, get_remote_default_branch_from_symbolic_ref);被 1 处调用(branch_diff_stats_to_default_branch)。

get_remote_default_branch_from_symbolic_ref243–266 ↗
async fn get_remote_default_branch_from_symbolic_ref(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
    remote: &str,
) -> Option<DefaultBranch>

作用:通过远程的 HEAD 指针判断默认分支,例如 origin/HEAD 指向 origin/main。它还会确认这个引用真的存在,避免用到过期指针。

数据流:输入是命令执行器、目录和远程名。它运行 git symbolic-ref --quiet refs/remotes/<remote>/HEAD,从输出里取出真正的远程分支引用,并用 git_ref_exists 验证它存在。通过验证就输出 DefaultBranch,否则输出 None。

调用关系:get_default_branch 会先用这种方式查每个远程仓库,因为它直接、可靠。它需要 run_git_command 读取指针,需要 git_ref_exists 做安全检查。

调用图:调用 2 个内部函数(git_ref_exists, run_git_command);被 1 处调用(get_default_branch);外部调用 1 个(format!)。

get_remote_default_branch_from_remote_show273–300 ↗
async fn get_remote_default_branch_from_remote_show(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
    remote: &str,
) -> Option<DefaultBranch>

作用:当远程 HEAD 指针没配好时,用 git remote show 的文字输出找默认分支。这是一个备用办法。

数据流:输入是命令执行器、目录和远程名。它运行 git remote show <remote>,逐行寻找 HEAD branch: 这一行,取出分支名,拼成远程跟踪引用,再用 git_ref_exists 确认本地真的有这个引用。成功就输出 DefaultBranch,失败就输出 None。

调用关系:get_default_branch 在 symbolic-ref 查不到时会调用它。它继续把命令执行交给 run_git_command,把引用存在性判断交给 git_ref_exists。

调用图:调用 2 个内部函数(git_ref_exists, run_git_command);被 1 处调用(get_default_branch);外部调用 1 个(format!)。

get_default_branch_local303–317 ↗
async fn get_default_branch_local(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<DefaultBranch>

作用:如果远程默认分支完全找不到,就退而求其次看看本地有没有 main 或 master。这保证一些没有远程配置的仓库也能显示改动统计。

数据流:输入是命令执行器和目录。它依次检查 refs/heads/mainrefs/heads/master 是否存在;第一个存在的会被包装成 DefaultBranch 输出。两个都没有就输出 None。

调用关系:get_default_branch 的最后兜底步骤会调用它。它只负责本地候选分支,引用检查交给 git_ref_exists。

调用图:调用 1 个内部函数(git_ref_exists);被 1 处调用(get_default_branch);外部调用 1 个(format!)。

git_ref_exists320–332 ↗
async fn git_ref_exists(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
    reference: &str,
) -> bool

作用:检查某个 Git 引用是否真的存在。这里的引用可以理解为 Git 里指向某个提交的一张“标签纸”,比如分支名或远程分支名。

数据流:输入是命令执行器、目录和引用字符串。它运行 git rev-parse --verify --quiet <reference>;命令成功就返回 true,失败或执行出错就返回 false。

调用关系:多个默认分支发现函数都会用它做最后确认,防止后面拿不存在的分支去比较。它把具体 Git 命令交给 run_git_command。

调用图:调用 1 个内部函数(run_git_command);被 3 处调用(get_default_branch_local, get_remote_default_branch_from_remote_show, get_remote_default_branch_from_symbolic_ref)。

open_pull_request339–348 ↗
async fn open_pull_request(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<StatusLinePullRequest>

作用:寻找当前检出内容对应的打开状态 PR。它先用当前分支查,查不到再用当前提交查,兼顾普通分支和 fork 工作流。

数据流:输入是命令执行器和目录。它先调用 open_pull_request_for_current_branch;如果得到了 PR,就直接返回。否则调用 open_pull_request_for_head_commit,尝试按 HEAD 提交去 GitHub 查关联 PR。

调用关系:status_line_git_summary 间接依赖它提供 PR 信息,相关测试会直接调用它。它是 PR 查询的调度点,把具体两种查法交给两个子函数。

调用图:调用 2 个内部函数(open_pull_request_for_current_branch, open_pull_request_for_head_commit);被 2 处调用(open_pull_request_falls_back_to_parent_repo_commit_lookup, open_pull_request_uses_current_branch_view_first)。

open_pull_request_for_current_branch351–362 ↗
async fn open_pull_request_for_current_branch(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<StatusLinePullRequest>

作用:用 GitHub CLI 的 gh pr view 查询当前分支对应的 PR。这种方法便宜、直接,符合用户平时在命令行里查 PR 的方式。

数据流:输入是命令执行器和目录。它运行 gh pr view --json number,url,state,成功后把 JSON 字符串交给 pull_request_from_view_output 解析;只有打开状态的 PR 才会输出,否则输出 None。

调用关系:open_pull_request 会优先调用它。它负责拿到 GitHub CLI 输出,判断开放状态和提取号码、链接的细节交给 pull_request_from_view_output。

调用图:调用 2 个内部函数(pull_request_from_view_output, run_gh_command);被 1 处调用(open_pull_request)。

open_pull_request_for_head_commit365–392 ↗
async fn open_pull_request_for_head_commit(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<StatusLinePullRequest>

作用:按当前 HEAD 提交去 GitHub 查关联 PR,用来处理分支查不到但提交其实属于某个 PR 的情况,尤其适合 fork 仓库场景。

数据流:输入是命令执行器和目录。它先用 current_head_sha 拿当前提交哈希值,再用 gh_repo_search_order 得到应该查询的仓库顺序;然后对每个仓库调用 GitHub API 的 commits/<sha>/pulls 接口。遇到第一个打开的 PR 就输出,全部没有就输出 None。

调用关系:open_pull_request 在当前分支查法失败后调用它。它把拿提交号交给 current_head_sha,把仓库顺序交给 gh_repo_search_order,把 API 输出解析交给 pull_request_from_api_output。

调用图:调用 4 个内部函数(current_head_sha, gh_repo_search_order, pull_request_from_api_output, run_gh_command);被 1 处调用(open_pull_request);外部调用 1 个(format!)。

current_head_sha395–404 ↗
async fn current_head_sha(runner: &dyn WorkspaceCommandExecutor, cwd: &Path) -> Option<String>

作用:读取当前 HEAD 的提交哈希值,也就是当前检出代码所对应的提交身份证号。提交查 PR 时必须先知道这个号。

数据流:输入是命令执行器和目录。它运行 git rev-parse HEAD,成功后去掉输出空白;如果结果非空,就输出这个 SHA 字符串,否则输出 None。

调用关系:open_pull_request_for_head_commit 会先调用它。它只负责问 Git 当前提交是什么,命令执行由 run_git_command 完成。

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

gh_repo_search_order407–423 ↗
async fn gh_repo_search_order(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
) -> Option<Vec<String>>

作用:决定按哪些 GitHub 仓库去查提交关联 PR,并且优先查父仓库。这样在 fork 上工作时,也能找到开在上游仓库的 PR。

数据流:输入是命令执行器和目录。它运行 gh repo view --json nameWithOwner,parent,拿到当前仓库和父仓库信息的 JSON,再交给 repo_search_order_from_output 解析成仓库名列表。

调用关系:open_pull_request_for_head_commit 需要它来知道 GitHub API 要查哪些仓库。它负责获取原始信息,排序和去重规则由 repo_search_order_from_output 处理。

调用图:调用 2 个内部函数(repo_search_order_from_output, run_gh_command);被 1 处调用(open_pull_request_for_head_commit)。

pull_request_from_view_output426–435 ↗
fn pull_request_from_view_output(stdout: &str) -> Option<StatusLinePullRequest>

作用:把 gh pr view 返回的 JSON 文本变成状态栏可用的 PR 信息。它只接受 open 状态,关闭或已合并的 PR 会被忽略。

数据流:输入是一段 JSON 字符串。它尝试解析出 PR 号码、URL 和状态;如果状态不区分大小写等于 open,就输出 StatusLinePullRequest,否则输出 None。

调用关系:open_pull_request_for_current_branch 拿到 GitHub CLI 输出后会调用它。它是分支查 PR 这条路上的过滤器,保证状态栏不显示已关闭 PR。

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

pull_request_from_api_output438–447 ↗
fn pull_request_from_api_output(stdout: &str) -> Option<StatusLinePullRequest>

作用:把 GitHub API 的“某个提交关联的 PR 列表”解析出来,并挑第一个打开状态的 PR。它用于分支查询失败后的兜底查询。

数据流:输入是 GitHub API 返回的 JSON 数组字符串。它解析成 PR 列表,逐个找 state 为 open 的项,然后输出号码和网页 URL;解析失败或没有打开 PR 就输出 None。

调用关系:open_pull_request_for_head_commit 每查一个仓库的 API 响应后会调用它。它负责从可能有多个结果的列表中挑出状态栏该显示的那个。

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

repo_search_order_from_output453–469 ↗
fn repo_search_order_from_output(stdout: &str) -> Option<Vec<String>>

作用:把 gh repo view 的 JSON 输出转成查询仓库顺序:父仓库在前,当前仓库在后,并避免重复。这样更符合上游 PR 的常见工作流。

数据流:输入是一段仓库信息 JSON。它解析出 parent.nameWithOwner 和 nameWithOwner;如果有父仓库先加入,再加入当前仓库且不重复。最后输出非空列表;如果两个都没有或解析失败,就输出 None。

调用关系:gh_repo_search_order 获取命令输出后会调用它。测试 tests::status_line_pr_fallback_searches_parent_repo_first 会验证父仓库确实排在前面。

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

run_git_command472–487 ↗
async fn run_git_command(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
    args: &[&str],
) -> Result<WorkspaceCommandOutput, crate::workspace_command::WorkspaceCommandError>

作用:统一通过工作区命令执行器运行 git 命令。它像一个标准出口,保证所有 Git 探测都用同样的 cwd 和环境设置。

数据流:输入是命令执行器、目录和 git 参数列表。它把 git 放到参数最前面,设置工作目录,并设置 GIT_OPTIONAL_LOCKS=0,意思是尽量不要为了读状态而拿 Git 锁;然后调用 runner.run,输出命令结果或错误。

调用关系:这个文件里所有需要 Git 的函数都通过它走,包括分支名、默认分支、提交号和引用检查。这样上层函数不用关心本地还是远程执行命令。

调用图:调用 1 个内部函数(new);被 7 处调用(branch_diff_stats_to_default_branch, current_branch_name, current_head_sha, get_git_remotes, get_remote_default_branch_from_remote_show, get_remote_default_branch_from_symbolic_ref, git_ref_exists);外部调用 3 个(to_path_buf, with_capacity, run)。

run_gh_command493–509 ↗
async fn run_gh_command(
    runner: &dyn WorkspaceCommandExecutor,
    cwd: &Path,
    args: &[&str],
) -> Result<WorkspaceCommandOutput, crate::workspace_command::WorkspaceCommandError>

作用:统一通过工作区命令执行器运行 gh 命令,也就是 GitHub CLI。它会关闭交互提示,避免后台状态栏查询突然要求用户输入。

数据流:输入是命令执行器、目录和 gh 参数列表。它把 gh 放到参数最前面,设置工作目录,并设置 GH_PROMPT_DISABLED=1GIT_TERMINAL_PROMPT=0;然后调用 runner.run,输出命令结果或错误。

调用关系:PR 查询相关函数都通过它访问 GitHub CLI,包括当前分支 PR、仓库信息和提交关联 PR。它让后台查询失败时安静返回,而不是卡住等待登录或输入。

调用图:调用 1 个内部函数(new);被 3 处调用(gh_repo_search_order, open_pull_request_for_current_branch, open_pull_request_for_head_commit);外部调用 3 个(to_path_buf, with_capacity, run)。

tests::branch_diff_stats_prefers_remote_default_ref_over_stale_local_branch521–569 ↗
async fn branch_diff_stats_prefers_remote_default_ref_over_stale_local_branch()

作用:测试分支改动统计会优先使用远程默认分支,而不是可能过期的本地分支。这能防止状态栏显示夸大的改动数字。

数据流:它准备一个 FakeRunner,预设一串 git 命令回应:远程 origin 的 HEAD 指向 origin/main,merge-base 返回 base-sha,diff 返回新增 1 行。调用 branch_diff_stats_to_default_branch 后,断言结果是新增 1、删除 0,并确认 merge-base 用的是远程引用。

调用关系:这是 branch_diff_stats_to_default_branch 的保护性测试。它用 tests::FakeRunner::new 和 tests::response 搭出假命令环境,再检查主逻辑是否按正确路径执行。

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

tests::open_pull_request_uses_current_branch_view_first572–591 ↗
async fn open_pull_request_uses_current_branch_view_first()

作用:测试查 PR 时会先走当前分支的 gh pr view,因为这是最直接、最省事的方式。

数据流:它让 FakeRunner 对 gh pr view 返回一个 open 状态 PR。调用 open_pull_request 后,断言拿到对应号码和 URL,并确认没有继续执行 git rev-parse HEAD,也就是没有进入提交兜底查询。

调用关系:这是 open_pull_request 的优先级测试。它证明 open_pull_request 会先交给 open_pull_request_for_current_branch,成功后就不再调用后续路径。

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

tests::open_pull_request_falls_back_to_parent_repo_commit_lookup594–642 ↗
async fn open_pull_request_falls_back_to_parent_repo_commit_lookup()

作用:测试当前分支查 PR 失败时,会按当前提交去父仓库里找 PR。这个场景常见于 fork 仓库开发。

数据流:它预设 gh pr view 失败,然后 Git 返回 head-sha,GitHub 仓库信息显示当前仓库有父仓库 openai/codex,最后父仓库 API 返回一个 open PR。调用 open_pull_request 后,断言拿到该 PR,并确认确实请求了父仓库的提交 PR 接口。

调用关系:这是 open_pull_request 的兜底路径测试。它覆盖 open_pull_request_for_head_commit、current_head_sha、gh_repo_search_order 和 pull_request_from_api_output 的配合。

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

tests::status_line_pr_view_parser_requires_open_pr645–662 ↗
fn status_line_pr_view_parser_requires_open_pr()

作用:测试 gh pr view 的解析器只接受打开状态的 PR。这样状态栏不会显示已经合并或关闭的 PR。

数据流:它分别传入两个 JSON:一个 state 是 OPEN,另一个 state 是 MERGED。前者应该解析成 StatusLinePullRequest,后者应该返回 None。

调用关系:这是 pull_request_from_view_output 的单元测试。它不跑外部命令,只验证 JSON 解析和状态过滤规则。

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

tests::status_line_pr_fallback_searches_parent_repo_first665–672 ↗
fn status_line_pr_fallback_searches_parent_repo_first()

作用:测试提交查 PR 的仓库顺序会把父仓库放在当前 fork 仓库前面。这能更容易找到真正开在上游项目里的 PR。

数据流:它传入一个包含当前仓库 fcoury/codex 和父仓库 openai/codex 的 JSON。解析结果应该是先 openai/codex,再 fcoury/codex。

调用关系:这是 repo_search_order_from_output 的单元测试。它保护 fork 工作流下的查询顺序不被意外改坏。

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

tests::response674–683 ↗
fn response(argv: &[&str], exit_code: i32, stdout: &str) -> FakeResponse

作用:给测试快速创建一条假命令响应。它把“如果看到这些命令参数,就返回这个退出码和输出”包装成 FakeResponse。

数据流:输入是命令参数数组、退出码和标准输出文本。它把参数转成字符串列表,把退出码和 stdout 放进 WorkspaceCommandOutput,stderr 留空,最后输出 FakeResponse。

调用关系:各个异步测试用它批量准备 FakeRunner 的脚本化回应。它是测试夹具的一部分,不参与真实运行。

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

tests::FakeRunner::new696–701 ↗
fn new(responses: Vec<FakeResponse>) -> Self

作用:创建一个假的命令执行器,用来在测试里替代真实 git 和 gh。这样测试不会依赖电脑上真实仓库、网络或 GitHub 登录状态。

数据流:输入是一组 FakeResponse。它把这些响应放进受 Mutex 保护的队列里,并创建一个空的 seen 列表记录之后收到过哪些命令,最后输出 FakeRunner。

调用关系:测试函数会先用它搭好假环境,再把 FakeRunner 传给被测函数。后续 tests::FakeRunner::run 会消费这些预设响应,tests::FakeRunner::saw 会检查执行记录。

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

tests::FakeRunner::saw703–710 ↗
fn saw(&self, argv: &[&str]) -> bool

作用:检查测试中的假执行器是否见过某条命令。它用于确认代码有没有走到某个分支,比如是否真的调用了父仓库 API。

数据流:输入是一组期望的命令参数。它把参数转成字符串列表,然后在 seen 记录里查有没有完全相同的一项;找到返回 true,否则返回 false。

调用关系:多个测试在调用主逻辑后用它做行为断言。它只读取 FakeRunner 的执行历史,不触发新命令。

tests::FakeRunner::run714–737 ↗
fn run(
            &self,
            command: WorkspaceCommand,
        ) -> Pin<
            Box<
                dyn Future<Output = Result<WorkspaceCommandOutput, WorkspaceCommandError>>

作用:实现测试版的 WorkspaceCommandExecutor:收到命令后不真的执行,而是返回预先准备好的假结果。它让测试可重复、可控。

数据流:输入是一个 WorkspaceCommand。它先把命令参数记录到 seen,然后在 responses 队列里寻找 argv 完全匹配的假响应;找到后移除并返回其中的 WorkspaceCommandOutput。找不到就让测试失败,提醒有人调用了没准备好的命令。

调用关系:被测函数以为自己在通过 run_git_command 或 run_gh_command 跑真实命令,实际会走到这个 fake run。它支撑了本文件所有需要模拟外部命令的测试。

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

tui/src/app/agent_status_feed.rs源码 ↗
domain_logic当用户查看或打开 /agent 状态/选择界面时活跃

这个文件服务于终端界面里的 /agent 状态输出。子代理可能会产生很多事件,比如发消息、执行命令、改文件、调用工具等;如果全都原样显示,用户很难快速看懂。这里做的事像是给每个子代理写一张“小抄”:从事件缓存里倒着找最近的活动,去掉同一个事项的重复开始/结束记录,挑出最多 6 条能概括的内容,再把文字限制到一定长度。真正显示时,每个子代理先有一行标题,也就是它的路径;下面只展示最多 3 行预览,并且缩进,让界面整齐。如果某个子代理还没有活动,就显示“还没有最近活动”。它还实现了 HistoryCell,也就是终端历史区里的一块可显示内容:既能生成带颜色样式的行,也能生成去掉样式的纯文本行。

函数细节11
AgentStatusHistoryCell::new27–29 ↗
fn new(entries: Vec<AgentStatusThreadPreview>) -> Self

作用:创建一块 /agent 状态历史内容,把已经准备好的多个子代理预览放进去。调用方用它把“数据”包装成界面可以显示的单元。

数据流:进去的是一组 AgentStatusThreadPreview,也就是每个子代理的简短活动预览;它把这组预览存到 AgentStatusHistoryCell 里面;出来的是一个新的状态显示单元,之后可以被要求画到终端上。

调用关系:它是组装界面内容的入口之一。测试里的 agent_status_uses_bounded_buffered_activity、agent_status_uses_reasoning_summaries_only 会用它检查显示是否正确;实际流程里的 open_agent_picker 也会用它把子代理状态放进界面。

调用图:被 3 处调用(agent_status_uses_bounded_buffered_activity, agent_status_uses_reasoning_summaries_only, open_agent_picker)。

AgentStatusHistoryCell::display_lines33–58 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把整块 /agent 状态内容变成一行一行的终端文本,包含标题、子代理名称和最近活动预览。它决定用户最终在界面上看到什么。

数据流:进去的是可用宽度 width,以及对象里保存的子代理预览;它先放入 /agent 标题和“Sub-agents running”,如果没有子代理就写一行“没有子代理运行”;如果有,就逐个生成标题行、按宽度生成预览行,并给预览加缩进;出来的是带颜色、加粗、斜体等样式的 Line 列表。

调用关系:它是显示层真正取内容的地方。raw_lines 会调用它先生成完整显示行,再把样式去掉;在界面渲染时,它也代表这个 HistoryCell 应该如何被画出来。

调用图:被 1 处调用(raw_lines);外部调用 1 个(vec!)。

AgentStatusHistoryCell::raw_lines60–62 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成这块状态内容的纯文本版本,也就是不带颜色、不带终端样式的行。这样日志、复制或测试时能拿到干净文字。

数据流:进去的是这个状态单元本身;它先用 display_lines 按最大宽度生成完整内容,再交给 plain_lines 去掉样式;出来的是纯文本 Line 列表,不改变原对象。

调用关系:它依赖 display_lines 复用同一套内容生成规则,避免纯文本和界面显示不一致;随后把结果交给 plain_lines 做“去样式”这一步。

调用图:调用 2 个内部函数(display_lines, plain_lines)。

AgentStatusThreadPreview::from_store72–74 ↗
fn from_store(agent_path: String, store: &ThreadEventStore) -> Self

作用:从某个子代理的事件缓存里做出一份最近活动预览。调用方不用自己翻事件记录,只要给它子代理路径和缓存即可。

数据流:进去的是 agent_path 和 ThreadEventStore;它从 store.buffer 里倒序读取事件,也就是从最新事件开始看;然后把这些事件交给 from_events 筛选、去重、压缩成活动摘要;出来的是 AgentStatusThreadPreview。

调用关系:它是从真实事件缓存生成预览的常用入口。测试 agent_status_uses_bounded_buffered_activity、agent_status_uses_reasoning_summaries_only 和实际的 open_agent_picker 都会用它;具体挑选活动的细活交给 from_events。

调用图:被 3 处调用(agent_status_uses_bounded_buffered_activity, agent_status_uses_reasoning_summaries_only, open_agent_picker);外部调用 1 个(from_events)。

AgentStatusThreadPreview::empty76–78 ↗
fn empty(agent_path: String) -> Self

作用:为一个没有活动记录的子代理创建空预览。这样界面仍然能显示这个子代理存在,只是提示还没有最近活动。

数据流:进去的是 agent_path;它构造一个空的事件迭代器,再交给 from_events 按同样规则处理;出来的是 activity 为空的 AgentStatusThreadPreview。

调用关系:open_agent_picker 会在需要展示没有缓存活动的子代理时用它。它故意复用 from_events,让“空预览”和“从缓存来的预览”走同一套构造逻辑。

调用图:被 1 处调用(open_agent_picker);外部调用 2 个(from_events, empty)。

AgentStatusThreadPreview::from_events80–114 ↗
fn from_events(
        agent_path: String,
        events: impl Iterator<Item = &'a ThreadBufferedEvent>,
    ) -> Self

作用:这是把原始事件变成简短活动列表的核心筛选器。它只留下用户看得懂、值得显示的最近事项,并限制数量,避免状态页太长。

数据流:进去的是子代理路径和一串 ThreadBufferedEvent;它逐个查看事件,只关心“事项开始”和“事项完成”这两类通知,跳过请求、反馈、历史响应等不适合显示的内容;它用事项 id 去重,避免同一个事项开始和结束各出现一次;再调用 activity_summary 把事项变成一句话摘要,最多保留 6 条,最后反转顺序让显示接近正常时间顺序;出来的是带路径和活动摘要列表的 AgentStatusThreadPreview。

调用关系:from_store 和 empty 都把事件来源交给它处理。它在内部依赖 activity_summary 判断每种 ThreadItem 应该怎么概括,自己负责“从哪些事件里挑”和“挑多少”。

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

AgentStatusThreadPreview::title_line116–118 ↗
fn title_line(&self) -> Line<'static>

作用:生成某个子代理预览的标题行。标题行主要显示子代理路径,让用户知道下面几行活动属于谁。

数据流:进去的是这个预览对象里的 agent_path;它把路径包在反引号里,并加上项目符号和颜色样式;出来的是一行可显示的终端文本。

调用关系:display_lines 在遍历每个子代理预览时会需要这样的标题行。它只管标题,不管活动内容;活动内容由 preview_lines 生成。

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

AgentStatusThreadPreview::preview_lines120–132 ↗
fn preview_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把一个子代理的活动摘要整理成适合终端宽度的几行预览。它保证内容不会太宽,也不会显示太多行。

数据流:进去的是显示宽度 width 和对象里保存的 activity 列表;它把每条活动按宽度自动换行,去掉空行,给文字加暗色样式;如果总行数超过 3 行,就只保留最后 3 行;出来的是这些预览 Line,不改动原始活动列表。

调用关系:display_lines 会调用它取得每个子代理标题下面的内容。之后 display_lines 再把这些行交给 indent_preview_line 加缩进,让界面层次更清楚。

activity_summary135–196 ↗
fn activity_summary(item: &ThreadItem) -> Option<String>

作用:把各种不同类型的线程事项变成一句普通人能看懂的摘要。比如命令变成 $ ...,改文件变成“Updated N file(s)”,网页搜索变成“Web search: ...”。

数据流:进去的是一个 ThreadItem,也就是线程里发生的一件事;它按事项类型分别处理:消息和计划直接取文字,推理只取最后一条摘要,命令、工具、文件改动、搜索、看图等改写成简短描述;用户消息、提示钩子、睡眠这类不适合作为子代理状态的内容会返回空;需要限制长度的文字会交给 bounded_summary 或 truncate_text;出来的是 Some(摘要文字) 或 None。

调用关系:from_events 在挑到一个事项后会调用它,决定这个事项能不能显示、该显示成什么。它再把统一的长度和空白处理交给 bounded_summary,确保预览不会失控。

调用图:调用 2 个内部函数(bounded_summary, truncate_text);被 1 处调用(from_events);外部调用 1 个(format!)。

bounded_summary198–202 ↗
fn bounded_summary(summary: &str) -> Option<String>

作用:把一段摘要文字压到安全长度,并清理多余空白。它是状态预览的“限长器”,防止一条很长的命令或消息撑爆界面。

数据流:进去的是原始摘要字符串;它先用 truncate_text 截到最多 240 个字素左右,字素可以理解成用户眼里看到的一个字符单位;再把连续空白压成普通空格;如果结果不是空字符串,就返回 Some;如果清完后什么都没有,就返回 None。

调用关系:activity_summary 会在多种事项需要生成摘要时调用它。它不关心事项类型,只负责让最终文字短、干净、可显示。

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

indent_preview_line204–207 ↗
fn indent_preview_line(mut line: Line<'static>) -> Line<'static>

作用:给一行活动预览前面加四个空格,让它看起来像缩在子代理标题下面的内容。这样用户一眼能分清标题和详情。

数据流:进去的是一行 Line;它在这行最前面插入四个空格;出来的是同一行内容的缩进版本,原行会被修改后返回。

调用关系:display_lines 在拿到 preview_lines 的结果后,会用它给每条预览加缩进。它是一个很小的排版辅助函数,只服务于这块状态显示。

tui/src/bottom_pane/pending_input_preview.rs源码 ↗
domain_logicmain loop / render pass

在聊天式终端里,用户可能会在系统还忙着执行工具或等待结果时继续输入。没有这个预览区,用户会搞不清自己刚才打的话是不是丢了、什么时候会发出去。这个文件定义了 PendingInputPreview 这个小组件,像一个排队告示牌:先显示“下一次工具调用后会提交”的 steer,再显示“本轮结束后重试提交”的 rejected steer,最后显示普通排队消息。它会根据终端宽度自动换行,只展示每条内容的前几行,太长就用省略号。它还会在底部提示快捷键,比如按某个键编辑最后一条排队消息,或按 Esc 立刻打断并发送 steer。核心做法是先把这些文字整理成带缩进、样式和提示的行,再交给 ratatui 这个终端绘图库画到屏幕缓冲区里。

函数细节21
PendingInputPreview::new37–45 ↗
fn new() -> Self

作用:创建一个空的待输入预览组件。它默认没有任何待显示消息,但已经准备好了默认快捷键提示:Alt+上箭头用于编辑,Esc 用于打断并立即发送。

数据流:进去没有参数 → 它新建三个空列表,分别放待提交 steer、被拒后待重试 steer、普通排队消息,并设置默认按键 → 出来一个可以放进界面里使用的 PendingInputPreview。

调用关系:这是这个组件的起点。界面初始化或测试准备样例时都会先调用它;它内部借助 key_hint 里的 alt 和 plain 生成给用户看的快捷键标识。

调用图:调用 2 个内部函数(alt, plain);被 14 处调用(new, desired_height_empty, desired_height_one_message, long_url_like_message_does_not_expand_into_wrapped_ellipsis_rows, render_many_line_message, render_more_than_three_messages, render_multiline_pending_steer_uses_single_prefix_and_truncates, render_one_message, render_one_message_with_shift_left_binding, render_one_pending_steer (+4 more));外部调用 1 个(new)。

PendingInputPreview::set_edit_binding50–52 ↗
fn set_edit_binding(&mut self, binding: Option<key_hint::KeyBinding>)

作用:改掉底部“编辑最后一条排队消息”的快捷键提示。它只改显示出来的提示文字,不负责真正处理按键事件。

数据流:进去一个可选的按键绑定 → 它把组件里原来的 edit_binding 换成新的,或者设为没有提示 → 出来没有返回值,但之后渲染时底部提示会变。

调用关系:外层界面在配置按键时会用到它,例如 set_queued_message_edit_binding 会把实际采用的快捷键同步给这个预览组件,避免屏幕提示和真实操作不一致。

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

PendingInputPreview::set_interrupt_binding54–56 ↗
fn set_interrupt_binding(&mut self, binding: Option<key_hint::KeyBinding>)

作用:改掉“打断当前等待并立即发送 steer”的快捷键提示。这样不同终端或不同用户配置可以显示正确的按键。

数据流:进去一个可选的按键绑定 → 它更新 interrupt_binding 字段,或者关闭这条提示 → 出来没有返回值,但待提交 steer 的标题行会按新设置显示。

调用关系:外层的按键配置流程 set_keymap_bindings 会调用它。这个函数只负责让提示文字跟配置一致,真正按下键后的行为在别处处理。

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

PendingInputPreview::push_truncated_preview_lines58–68 ↗
fn push_truncated_preview_lines(
        lines: &mut Vec<Line<'static>>,
        wrapped: Vec<Line<'static>>,
        overflow_line: Line<'static>,
    )

作用:把一条消息的预览行塞进总列表里,并限制最多显示 3 行。超过 3 行时,它会额外放一行省略号,告诉用户后面还有内容没展示。

数据流:进去一个要追加的行列表、一组已经换好行的预览内容、以及超长时用的省略号行 → 它先加入最多 3 行内容,如果原内容更多就追加省略号 → 出来的效果是总行列表变长,但不会被长消息撑得太高。

调用关系:as_renderable 在处理 pending steer、rejected steer 和普通排队消息时都会用它。它像公告栏的限高规则,保证每条消息只占有限空间。

PendingInputPreview::push_section_header70–77 ↗
fn push_section_header(lines: &mut Vec<Line<'static>>, width: u16, header: Line<'static>)

作用:给某一类待发送内容添加标题行,比如“排队的后续输入”。标题前会带一个项目符号,并按当前终端宽度自动换行。

数据流:进去总行列表、可用宽度、标题文字 → 它给标题加上“• ”前缀和缩进规则,再调用自动换行工具处理 → 出来是若干标题行被追加到总行列表里。

调用关系:as_renderable 在每个分区开始时调用它。它把分区标题统一做成同一种样子,并把具体换行工作交给 adaptive_wrap_lines。

调用图:调用 2 个内部函数(new, adaptive_wrap_lines);外部调用 3 个(from, once, vec!)。

PendingInputPreview::as_renderable79–168 ↗
fn as_renderable(&self, width: u16) -> Box<dyn Renderable>

作用:把当前待显示的数据整理成一个真正能画到终端上的对象。它决定显示哪些分区、顺序是什么、文字怎么缩进、太长怎么省略、快捷键提示放在哪里。

数据流:进去当前组件状态和终端宽度 → 如果没有内容或宽度太窄,就返回一个空的可渲染对象;否则按 pending steers、rejected steers、queued messages 的顺序生成多行文字,逐条换行和截断,并追加必要的快捷键提示 → 出来一个 Renderable,也就是后续可以问高度、也可以画出来的界面对象。

调用关系:这是本文件的核心组装器。render 和 desired_height 都先找它要一个临时的可渲染对象;它再调用 push_section_header、push_truncated_preview_lines 和 adaptive_wrap_lines 完成具体排版。

调用图:调用 2 个内部函数(new, adaptive_wrap_lines);被 2 处调用(desired_height, render);外部调用 6 个(new, from, new, push_section_header, push_truncated_preview_lines, vec!)。

PendingInputPreview::render172–178 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把这个预览组件画到终端屏幕的指定区域里。区域为空时它什么也不做,避免无意义绘制。

数据流:进去一个屏幕矩形区域和屏幕缓冲区 → 它先检查区域是不是空的,再按区域宽度生成可渲染内容,并把内容画进缓冲区 → 出来没有返回值,但缓冲区里的字符和样式被更新了。

调用关系:这是 Renderable 接口要求的绘制入口。终端界面每次刷新底部面板时会调用它;它把排版工作交给 as_renderable。

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

PendingInputPreview::desired_height180–182 ↗
fn desired_height(&self, width: u16) -> u16

作用:告诉外层布局:按当前宽度显示这个预览区需要多高。这样界面能提前分配空间,不至于文字被随便截断。

数据流:进去一个可用宽度 → 它用同样的排版规则生成可渲染对象,然后询问这个对象需要多少行 → 出来一个高度数字。

调用关系:这是 Renderable 接口里的尺寸计算入口。外层布局在真正绘制前会用它估算高度;它和 render 都走 as_renderable,所以算出来的高度和实际画出的内容保持一致。

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

tests::desired_height_empty192–195 ↗
fn desired_height_empty()

作用:确认空预览组件不占高度。也就是说,没有待显示内容时,底部不会多出一块空白。

数据流:进去没有外部输入 → 它创建空的 PendingInputPreview,询问宽度为 40 时的期望高度 → 结果应为 0,否则测试失败。

调用关系:这个测试覆盖 PendingInputPreview::new 和 desired_height 的最基本情况,防止空状态被误画出来。

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

tests::desired_height_one_message198–202 ↗
fn desired_height_one_message()

作用:确认只有一条普通排队消息时,高度计算是合理的。这里期望包含标题、消息本身和编辑提示三行。

数据流:进去没有外部输入 → 它新建组件,塞入一条“Hello, world!”消息,再计算宽度 40 下的高度 → 输出通过断言体现:高度必须是 3。

调用关系:这个测试保证 desired_height 和普通 queued_messages 的显示规则匹配,避免布局少留或多留空间。

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

tests::render_one_message205–213 ↗
fn render_one_message()

作用:检查一条普通排队消息实际画出来的样子。它用快照测试,也就是把渲染结果保存成标准答案,以后改动时对比。

数据流:进去没有外部输入 → 它创建组件、放入一条消息、按计算出的高度建立空缓冲区,然后调用渲染 → 出来是缓冲区内容被拿去和快照比对。

调用关系:这个测试走完整的 desired_height 加 render 流程,验证 as_renderable 生成的标题、消息缩进和底部编辑提示都没有变形。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_one_message_with_shift_left_binding216–228 ↗
fn render_one_message_with_shift_left_binding()

作用:检查当编辑快捷键被改成 Shift+左箭头时,界面提示也跟着变。这样能防止用户看到旧提示、按了却没反应。

数据流:进去没有外部输入 → 它创建组件、放入一条消息、设置新的编辑按键提示、渲染到缓冲区 → 出来的画面用快照确认包含新的按键提示。

调用关系:这个测试主要覆盖 set_edit_binding 对渲染结果的影响,同时也验证 key_hint::shift 生成的按键显示能被组件正常使用。

调用图:调用 2 个内部函数(new, shift);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_two_messages231–242 ↗
fn render_two_messages()

作用:检查两条普通排队消息会按顺序显示。它保证列表不是只显示第一条,也不会乱序。

数据流:进去没有外部输入 → 它创建组件、加入两条 queued_messages、计算高度并渲染 → 输出缓冲区会和快照比对,确认两条消息都在。

调用关系:这个测试覆盖 as_renderable 对普通消息列表的循环处理,确保每条排队输入都会被加入预览。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_more_than_three_messages245–262 ↗
fn render_more_than_three_messages()

作用:检查多条普通排队消息一起显示时的整体画面。重点是确认组件能处理较长队列,而不是只适合一两条。

数据流:进去没有外部输入 → 它创建组件、加入四条普通排队消息、渲染到缓冲区 → 出来的完整画面通过快照保存和比较。

调用关系:这个测试从外部表现上覆盖 queued_messages 多项渲染。它帮助发现分隔、缩进、提示行位置等 UI 细节被意外改坏。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_wrapped_message265–278 ↗
fn render_wrapped_message()

作用:检查长消息在窄一些的区域里会自动换行,而不是把终端撑乱或直接丢字。

数据流:进去没有外部输入 → 它创建组件,加入一条较长消息和一条普通消息,按宽度 40 渲染 → 输出画面用快照确认换行和缩进正常。

调用关系:这个测试间接覆盖 adaptive_wrap_lines 在普通消息预览中的使用,确保 as_renderable 给换行工具传入了正确的缩进规则。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_many_line_message281–291 ↗
fn render_many_line_message()

作用:检查一条本身就包含多行文本的消息如何显示。它保证用户粘贴多行内容时,预览不会乱成一团。

数据流:进去没有外部输入 → 它创建组件,加入包含换行符的消息,计算高度并渲染 → 出来的缓冲区与快照比较,确认多行和截断规则符合预期。

调用关系:这个测试覆盖 message.lines() 这条路径,也就是消息原本就分成多行时,as_renderable 仍然会统一加前缀、缩进和预览限制。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::long_url_like_message_does_not_expand_into_wrapped_ellipsis_rows294–323 ↗
fn long_url_like_message_does_not_expand_into_wrapped_ellipsis_rows()

作用:检查很长、像网址一样没有空格的文本不会产生奇怪的省略号行。用户粘贴链接时,预览应该紧凑而稳定。

数据流:进去没有外部输入 → 它创建组件,放入一条超长 URL 风格字符串,在宽度 36 下计算高度并渲染,然后逐行读取缓冲区字符 → 输出通过断言确认高度是 3,并且没有出现省略号行。

调用关系:这个测试专门守住一个容易出错的边界情况:长 token 的换行不应被误判成多行预览截断。它同时验证 desired_height、render 和缓冲区内容。

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

tests::render_one_pending_steer326–334 ↗
fn render_one_pending_steer()

作用:检查一条 pending steer 的显示效果。pending steer 是一种稍后自动提交的引导消息,和普通排队消息的提示不同。

数据流:进去没有外部输入 → 它创建组件,加入一条 pending_steers,计算高度并渲染 → 输出画面通过快照确认标题、打断提示和内容缩进正确。

调用关系:这个测试覆盖 as_renderable 的 pending_steers 分支,确保这类消息显示在正确的分区里,并带有立即发送的快捷键提示。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_one_pending_steer_with_remapped_interrupt_binding337–349 ↗
fn render_one_pending_steer_with_remapped_interrupt_binding()

作用:检查 pending steer 的“立即打断发送”快捷键改掉后,标题里的提示也会改。这里把默认 Esc 改成 F12 来验证。

数据流:进去没有外部输入 → 它创建组件,加入一条 pending steer,设置新的 interrupt_binding,然后渲染 → 输出缓冲区和快照比对,确认提示显示新按键。

调用关系:这个测试主要覆盖 set_interrupt_binding 对 pending steer 标题的影响,并验证 key_hint::plain 配合功能键显示没有问题。

调用图:调用 2 个内部函数(new, plain);外部调用 4 个(empty, F, new, assert_snapshot!)。

tests::render_pending_steers_above_queued_messages352–372 ↗
fn render_pending_steers_above_queued_messages()

作用:检查不同类别的待发送内容会按正确顺序显示:pending steers 在前,rejected steers 居中,普通排队消息在后。

数据流:进去没有外部输入 → 它创建组件,分别加入 pending steer、rejected steer 和 queued message,渲染完整预览 → 输出画面用快照确认分区顺序、空行和提示位置都正确。

调用关系:这个测试覆盖 as_renderable 的三大分区同时存在的情况。它保证这个“排队告示牌”的优先级展示不会被后续改动打乱。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tests::render_multiline_pending_steer_uses_single_prefix_and_truncates375–388 ↗
fn render_multiline_pending_steer_uses_single_prefix_and_truncates()

作用:检查多行 pending steer 只在开头使用一次箭头前缀,并且超过预览上限会截断。这样长 steer 看起来整齐,不会占满底部区域。

数据流:进去没有外部输入 → 它创建组件,加入一条包含四行的 pending steer,按宽度 48 渲染 → 输出画面通过快照确认前缀、缩进和省略号截断规则正确。

调用关系:这个测试重点覆盖 pending_steers 分支里 push_truncated_preview_lines 的效果,防止多行 steer 在 UI 上过度展开。

调用图:调用 1 个内部函数(new);外部调用 3 个(empty, new, assert_snapshot!)。

tui/src/bottom_pane/status_line_style.rs源码 ↗
domain_logicrendering / status line update

底部状态栏就像软件界面最下面的一排小提示牌:告诉你当前模型、目录、分支、状态、额度等信息。这个文件解决的问题是:这些信息不能只是硬拼成一串字,还要按类型上合适的颜色,中间加清楚的分隔符,并且在用户选择不用主题色时退回到低调显示。它先把每个状态栏项目归到一种“强调类别”,比如模型、路径、分支、用量。然后尝试从主题里找颜色;找不到就用备用颜色。为了避免状态栏太刺眼,它还会把颜色稍微“压柔和”,类似把荧光笔换成淡一点的马克笔。最后它把文字片段组装成 ratatui 的 Line(一行可带样式的终端文本)。如果没有任何片段,就返回空,避免画出没意义的状态栏。

函数细节16
StatusLineAccent::for_item31–55 ↗
fn for_item(item: StatusLineItem) -> Self

作用:把一个具体的状态栏项目,归类成一种颜色强调类别。比如模型名归到“模型”,当前目录归到“路径”,Git 分支归到“分支”。

数据流:输入是一个 StatusLineItem,也就是状态栏上的某个信息项 → 函数查看它属于哪一类信息 → 输出一个 StatusLineAccent,后面会用它来决定该用什么颜色风格。

调用关系:它是状态栏上色流程的第一步。status_line_from_segments_with_resolver 在处理每段文字时会先调用它,把“具体项目”翻译成“颜色类别”,然后再继续找主题颜色或备用颜色。

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

StatusLineAccent::scopes57–70 ↗
fn scopes(self) -> &'static [&'static str]

作用:给每种状态栏颜色类别配一组主题查找关键词。这里的 scope 可以理解成“向主题系统要颜色时用的标签”。

数据流:输入是一个 StatusLineAccent → 函数根据类别返回一小组固定字符串标签 → 调用方可以拿这些标签去主题里找对应的前景色。

调用关系:它服务于 status_line_from_segments 里的主题颜色查找。外层函数把这些标签交给 foreground_style_for_scopes,让状态栏颜色尽量和编辑器主题保持一致。

StatusLineAccent::fallback_style72–78 ↗
fn fallback_style(self) -> Style

作用:当主题里找不到合适颜色时,给每种类别一个安全的备用颜色。这样状态栏不会因为主题缺项而变得没有区分度。

数据流:输入是一个 StatusLineAccent → 函数选择青色、绿色或洋红色等默认样式 → 输出一个 Style,也就是终端文字的显示样式。

调用关系:它在主题颜色缺失时兜底。status_line_from_segments_with_resolver 会优先用主题返回的样式,如果没有,就改用这里给出的备用样式。

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

status_line_from_segments81–91 ↗
fn status_line_from_segments(
    segments: I,
    use_theme_colors: bool,
) -> Option<Line<'static>>

作用:这是外部最常用的入口:把一组“状态栏项目 + 文字”变成真正可以渲染的一行状态栏。调用者不需要关心主题颜色怎么找。

数据流:输入是一批状态栏片段,以及是否启用主题颜色的开关 → 它准备好默认的主题颜色查找办法 → 把实际组装工作交给 status_line_from_segments_with_resolver → 输出一行带样式的 Line,或者在没有内容时输出 None。

调用关系:它是公开包装层。它不自己拼细节,而是调用 status_line_from_segments_with_resolver;这样正常运行用真实主题,测试时则可以换成假的颜色查找器。

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

status_line_from_segments_with_resolver93–124 ↗
fn status_line_from_segments_with_resolver(
    segments: I,
    use_theme_colors: bool,
    theme_style_for_accent: F,
) -> Option<Line<'static>>

作用:真正组装状态栏的核心函数。它负责加分隔符、选颜色、弱化颜色、给 PR 编号加下划线,并把所有片段拼成一行。

数据流:输入是一批片段、一个是否使用主题色的开关、一个“按类别找样式”的函数 → 它按顺序遍历片段,在每两段之间插入灰一点的“ · ”分隔符;每段文字根据项目类型取主题样式或备用样式,不用主题时统一变暗;PR 编号额外加下划线 → 输出 Line;如果输入为空,就输出 None。

调用关系:这是整个文件的装配车间。status_line_from_segments 会调用它,多个测试也直接调用它来检查各种边界情况。它内部会用 StatusLineAccent::for_item 做分类,再用 soften_status_line_style 把颜色调柔和,最后创建 Span 和 Line 给界面渲染。

调用图:调用 2 个内部函数(for_item, soften_status_line_style);被 6 处调用(status_line_from_segments, pull_request_number_uses_link_style, status_line_segments_can_disable_theme_colors, status_line_segments_dim_separators_and_use_theme_styles_first, status_line_segments_preserve_order_and_plain_text, status_line_segments_soften_rgb_theme_styles_without_dimming_text);外部调用 3 个(styled, default, new)。

soften_status_line_style126–131 ↗
fn soften_status_line_style(mut style: Style) -> Style

作用:把一个文字样式里的前景色调得柔和一点。它只碰颜色,不会随便改变其他样式属性。

数据流:输入是一个 Style → 如果这个样式有前景色,就把颜色交给 soften_status_line_color 处理;如果没有颜色,就保持原样 → 输出调整后的 Style。

调用关系:它夹在“拿到主题颜色”和“显示到状态栏”之间。status_line_from_segments_with_resolver 调用它,目的是让主题色适合状态栏这种辅助信息区域,不要太亮太抢。

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

soften_status_line_color134–163 ↗
fn soften_status_line_color(color: Color) -> Color

作用:把具体颜色变得不那么刺眼。对 RGB 颜色会按亮度重新混合;对一些亮色名字会换成普通色。

数据流:输入是一个 Color → 如果是 RGB 红绿蓝数值,它先用 weighted_luma 算出人眼感受到的亮度,再用 soften_rgb_channel 分别调整红、绿、蓝三个通道;如果是 LightRed 这类亮色,就换成 Red 这类普通色;其他颜色保持不变 → 输出新的 Color。

调用关系:它是颜色柔化的具体执行者,由 soften_status_line_style 调用。它还会把 RGB 情况拆给 weighted_luma 和 soften_rgb_channel 这两个小工具处理。

调用图:调用 2 个内部函数(soften_rgb_channel, weighted_luma);被 1 处调用(soften_status_line_style);外部调用 1 个(Rgb)。

weighted_luma165–167 ↗
fn weighted_luma(r: u8, g: u8, b: u8) -> u16

作用:估算一个 RGB 颜色在人眼看来有多亮。因为人眼对绿色更敏感,所以红、绿、蓝不是平均算。

数据流:输入是红、绿、蓝三个 0 到 255 的数值 → 它按固定权重计算亮度,绿色权重最高、蓝色最低 → 输出一个亮度数值,供后续柔化颜色使用。

调用关系:它只在 soften_status_line_color 处理 RGB 颜色时使用。算出来的亮度会传给 soften_rgb_channel,用来把颜色往更平衡、更柔和的方向拉。

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

soften_rgb_channel169–177 ↗
fn soften_rgb_channel(channel: u8, luma: u16) -> u8

作用:调整 RGB 颜色中的单个通道,比如只调整红色值。它把原本很纯、很冲的颜色往整体亮度靠一点。

数据流:输入是某个颜色通道值和整体亮度 luma → 它按设定的饱和度比例,把通道值和亮度混合,再按亮度比例做最终调整 → 输出新的通道值。

调用关系:它被 soften_status_line_color 调用三次,分别处理红、绿、蓝。三个处理后的值重新组成一个更柔和的 RGB 颜色。

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

tests::line_text185–190 ↗
fn line_text(line: &Line<'static>) -> String

作用:测试用的小帮手:从带样式的一行里只取出纯文字。这样测试可以确认文字顺序和分隔符对不对,不被颜色样式干扰。

数据流:输入是一个 Line → 它遍历里面所有 Span,把每段的文字内容取出来并连在一起 → 输出一个普通字符串。

调用关系:它只服务测试代码。多个测试会用它把状态栏 Line 还原成纯文本,再和预期字符串比较。

tests::status_line_segments_preserve_order_and_plain_text193–212 ↗
fn status_line_segments_preserve_order_and_plain_text()

作用:确认状态栏会按输入顺序显示文字,并且分隔符是预期的“ · ”。同时检查没有主题颜色时会用正确的备用颜色。

数据流:输入是模型名、当前目录、Git 分支三段测试数据 → 测试调用 status_line_from_segments_with_resolver,并让主题查找总是失败 → 得到一行状态栏后,检查纯文字和每段颜色是否符合预期。

调用关系:这是对核心组装函数的基础测试。它直接调用 status_line_from_segments_with_resolver,验证顺序、分隔符和 fallback_style 这条兜底路径。

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

tests::status_line_segments_dim_separators_and_use_theme_styles_first215–234 ↗
fn status_line_segments_dim_separators_and_use_theme_styles_first()

作用:确认状态栏会优先使用主题给出的颜色,同时分隔符会变暗,正文不会被误变暗。

数据流:输入是模型名和上下文用量两段测试数据,并提供一个只给模型类别返回红色的假主题 → 函数生成状态栏 → 测试检查模型用了主题红色,分隔符是暗的,用量段则退回备用绿色。

调用关系:它直接测试 status_line_from_segments_with_resolver 的颜色选择顺序:先问主题,主题没有再用备用样式。它也顺便验证分隔符的低调显示。

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

tests::status_line_segments_soften_rgb_theme_styles_without_dimming_text238–248 ↗
fn status_line_segments_soften_rgb_theme_styles_without_dimming_text()

作用:确认主题给的 RGB 颜色会被调柔和,但正文不会因此被加上“变暗”效果。

数据流:输入是一段模型名,并让假主题返回纯红 RGB(255, 0, 0) → 生成状态栏时颜色会经过柔化 → 测试检查输出颜色变成较柔和的红,并确认没有 DIM 变暗标记。

调用关系:它覆盖 soften_status_line_style、soften_status_line_color、weighted_luma 和 soften_rgb_channel 这一串颜色柔化路径,入口仍然是 status_line_from_segments_with_resolver。

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

tests::status_line_segments_can_disable_theme_colors251–268 ↗
fn status_line_segments_can_disable_theme_colors()

作用:确认用户关闭主题颜色后,状态栏不会偷偷使用主题颜色,而是统一用低调的暗色样式。

数据流:输入是两段状态栏文字,并传入 use_theme_colors 为 false;即使假主题返回红色,也应该被忽略 → 输出状态栏 → 测试检查文字正确、各段都没有前景色,并带有 DIM 变暗标记。

调用关系:它测试 status_line_from_segments_with_resolver 的开关行为。这个测试保证“禁用主题颜色”是真正生效的,而不是只影响一部分片段。

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

tests::status_line_segments_return_none_when_empty290–299 ↗
fn status_line_segments_return_none_when_empty()

作用:确认没有任何状态栏片段时,函数不会生成一条空状态栏。这样渲染层可以直接知道“不用画”。

数据流:输入是空的片段列表 → 调用目标逻辑后预期得到 None → 测试用断言确认结果确实为空。

调用关系:它验证核心组装函数的空输入边界。虽然调用事实里只记录了断言宏,但这个测试的目标是保证 status_line_from_segments_with_resolver 在没有内容时不制造无意义的 Line。

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

tui/src/chatwidget/goal_status.rs源码 ↗
domain_logicmain loop / status rendering

这个文件解决的是一个界面显示问题:服务器告诉前端一个目标的完整状态,但底部状态栏空间很小,只能显示一个简短标记和一点用量信息。这里的 GoalStatusState 像一张快照,记住某个目标当时的状态,以及前端是在什么时候看到它的。这样如果目标还在进行中,界面刷新时就能把“从看到它到现在又过了多久”也算进去,不会让计时停住。核心函数会根据目标状态生成 GoalStatusIndicator,也就是底部栏真正要显示的东西。用量显示有一个重要规则:如果有 token 预算,就优先显示“用了多少 / 预算多少”。token 可以理解成模型读写文字时消耗的小单位。如果没有 token 预算,就显示已经花了多少时间。目标完成、预算用完、暂停、卡住等状态也会用不同方式简化成用户能看懂的提示。

函数细节16
GoalStatusState::new18–20 ↗
fn new(goal: AppThreadGoal, observed_at: Instant) -> Self

作用:创建一份目标状态快照。调用方在收到服务器发来的目标信息时,用它把目标内容和“看到它的时间”绑在一起。

数据流:输入是一份目标数据和一个时间点 → 函数把它们放进 GoalStatusState 这个小容器里 → 输出这个容器本身,不额外改动别的东西。

调用关系:当线程目标更新时,外层的 on_thread_goal_updated 会用它保存最新状态;测试里的 active_goal_state 也用它快速造出一个进行中的目标。后续状态栏要显示时,会继续调用这个对象的 indicator

调用图:被 2 处调用(active_goal_state, on_thread_goal_updated)。

GoalStatusState::is_active22–24 ↗
fn is_active(&self) -> bool

作用:判断这个目标现在是不是“进行中”。这让外层界面可以快速知道它是否还需要按活动目标来处理。

数据流:输入是已经保存好的目标状态 → 函数只看里面的 status 字段是不是 Active → 输出 true 或 false,不修改任何数据。

调用关系:它是 GoalStatusState 上的一个便捷检查按钮。外层代码在需要区分“还在跑”和“已经停住或结束”时会用到它。

GoalStatusState::indicator26–42 ↗
fn indicator(
        &self,
        now: Instant,
        active_turn_started_at: Option<Instant>,
    ) -> Option<GoalStatusIndicator>

作用:把保存的目标快照转换成底部状态栏可以直接显示的状态提示。它还会在目标仍然活跃时,把当前这一轮已经过去的时间补进去。

数据流:输入是当前时间,以及当前对话轮次开始的时间(可能没有)→ 函数先复制目标,避免直接改掉原始快照;如果目标是进行中,就从“观察到目标的时间”和“本轮开始时间”里选较晚的那个作为计时起点,再把到现在的秒数加到已用时间里 → 最后输出一个可显示的 GoalStatusIndicator,原状态不被改动。

调用关系:这是状态栏渲染时最常用的入口。它负责补齐实时流逝的时间,然后把真正的状态分类工作交给 goal_status_indicator_from_app_goal。这样外层界面不用懂预算、计时和各种状态怎么格式化。

调用图:调用 1 个内部函数(goal_status_indicator_from_app_goal);外部调用 4 个(clone, max, saturating_duration_since, try_from)。

goal_status_indicator_from_app_goal45–66 ↗
fn goal_status_indicator_from_app_goal(
    goal: &AppThreadGoal,
) -> Option<GoalStatusIndicator>

作用:根据目标的状态,决定底部栏该显示哪一种提示。它把服务器给的详细目标,压缩成界面上的简短标识。

数据流:输入是一份目标数据 → 函数查看目标状态:进行中就准备进行中的用量,暂停、卡住、超限等就给出对应标记,完成或预算耗尽时再附上合适的用量文字 → 输出一个 GoalStatusIndicator,用于界面显示。

调用关系GoalStatusState::indicator 会在补完实时耗时后调用它。它自己不负责算实时经过多久,只负责把目标状态分派给 active_goal_usagestopped_goal_budget_usagecompleted_goal_usage 这些小格式化函数。

调用图:调用 3 个内部函数(active_goal_usage, completed_goal_usage, stopped_goal_budget_usage);被 1 处调用(indicator)。

active_goal_usage68–82 ↗
fn active_goal_usage(
    token_budget: Option<i64>,
    tokens_used: i64,
    time_used_seconds: i64,
) -> Option<String>

作用:生成“进行中目标”的用量文字。它决定用户看到的是 token 用量,还是已经花了多久。

数据流:输入是可选的 token 预算、已用 token 数、已用秒数 → 如果有预算,就把数字压短后做成“已用 / 预算”;如果没有预算,就把秒数变成类似“2m”的时间文字 → 输出一段可显示的字符串,包在 Some 里。

调用关系goal_status_indicator_from_app_goal 在目标处于 Active 时调用它。它会借用 format_tokens_compact 把大数字变短,也会借用 format_goal_elapsed_seconds 把秒数变成人能读的时间。

调用图:调用 1 个内部函数(format_goal_elapsed_seconds);被 1 处调用(goal_status_indicator_from_app_goal);外部调用 1 个(format!)。

stopped_goal_budget_usage84–92 ↗
fn stopped_goal_budget_usage(token_budget: Option<i64>, tokens_used: i64) -> Option<String>

作用:生成“因为预算限制停下”的用量文字。它只在确实有 token 预算时显示用量,没有预算就不硬编一个数字。

数据流:输入是可选的 token 预算和已用 token 数 → 如果预算存在,就生成“已用 / 预算 tokens”;如果预算不存在,就输出 None,表示这块不用显示 → 不修改任何外部状态。

调用关系goal_status_indicator_from_app_goal 在目标状态是 BudgetLimited 时调用它。它的作用是让预算耗尽的提示更具体,但也避免在没有预算信息时显示误导性内容。

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

completed_goal_usage94–104 ↗
fn completed_goal_usage(
    token_budget: Option<i64>,
    tokens_used: i64,
    time_used_seconds: i64,
) -> String

作用:生成“目标完成后”的最终用量文字。完成时如果有预算,就强调用了多少 token;没有预算,就显示总共花了多久。

数据流:输入是可选的 token 预算、已用 token 数、已用秒数 → 如果曾经有 token 预算,就输出类似“40K tokens”;否则把秒数格式化成总耗时 → 输出一段字符串。

调用关系goal_status_indicator_from_app_goal 在目标状态是 Complete 时调用它。它和进行中用量不同:完成后不再显示“已用 / 预算”的进度感,而是给用户一个收尾总结。

调用图:调用 1 个内部函数(format_goal_elapsed_seconds);被 1 处调用(goal_status_indicator_from_app_goal);外部调用 1 个(format!)。

tests::active_goal_usage_prefers_token_budget119–128 ↗
fn active_goal_usage_prefers_token_budget()

作用:验证进行中的目标在有 token 预算时,会优先显示 token 用量,而不是显示时间。

数据流:输入是预算 50,000、已用 12,500、耗时 90 秒这些测试数据 → 调用 active_goal_usage → 检查输出是不是“12.5K / 50K”。

调用关系:这是给 active_goal_usage 的规则兜底的测试。它保证以后有人改格式化逻辑时,不会不小心把有预算的目标改成显示时间。

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

tests::active_goal_usage_reports_time_without_budget131–139 ↗
fn active_goal_usage_reports_time_without_budget()

作用:验证进行中的目标如果没有 token 预算,就会显示耗时。

数据流:输入是没有预算、已用 token 数和 120 秒耗时 → 调用 active_goal_usage → 检查结果是不是“2m”。

调用关系:这个测试补上了 active_goal_usage 的另一条路:没有预算时走时间显示。它保护的是无预算目标在状态栏里的基本可读性。

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

tests::stopped_goal_budget_usage_reports_budgeted_tokens142–147 ↗
fn stopped_goal_budget_usage_reports_budgeted_tokens()

作用:验证预算受限时,如果有预算信息,会显示已用和预算的 token 数。

数据流:输入是预算 50,000、已用 63,876 → 调用 stopped_goal_budget_usage → 检查输出是不是“63.9K / 50K tokens”。

调用关系:这个测试守住 BudgetLimited 状态的显示格式。它确保用户能看出目标是因为 token 用量超过预算而停下的。

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

tests::stopped_goal_budget_usage_omits_unbudgeted_usage150–155 ↗
fn stopped_goal_budget_usage_omits_unbudgeted_usage()

作用:验证没有预算时,预算受限用量函数不会乱显示一段不存在的预算信息。

数据流:输入是没有 token 预算和一个已用 token 数 → 调用 stopped_goal_budget_usage → 检查输出是 None

调用关系:这个测试防止界面出现误导信息。它和前一个测试一起覆盖了 stopped_goal_budget_usage 的有预算、无预算两种情况。

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

tests::completed_goal_usage_reports_tokens_when_budgeted158–167 ↗
fn completed_goal_usage_reports_tokens_when_budgeted()

作用:验证目标完成后,如果有 token 预算,就显示最终用了多少 token。

数据流:输入是预算 50,000、已用 40,000、耗时 120 秒 → 调用 completed_goal_usage → 检查输出是不是“40K tokens”。

调用关系:这个测试保护完成状态的总结规则。它确保有预算的目标完成后,界面重点展示 token 消耗,而不是耗时。

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

tests::completed_goal_usage_reports_time_without_token_budget170–178 ↗
fn completed_goal_usage_reports_time_without_token_budget()

作用:验证目标完成后,如果没有 token 预算,就显示总耗时。

数据流:输入是没有预算、已用 40,000 token、耗时 36,720 秒 → 调用 completed_goal_usage → 检查输出是不是“10h 12m”。

调用关系:这个测试覆盖 completed_goal_usage 的无预算分支。它保证完成状态在没有预算时仍然能给用户一个清楚的结果总结。

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

tests::active_goal_status_includes_current_turn_elapsed_time181–194 ↗
fn active_goal_status_includes_current_turn_elapsed_time()

作用:验证活动目标的状态栏会把当前这一轮新过去的时间也算进去,而不是只显示服务器上次给的旧时间。

数据流:测试先取一个当前时间,造出一个已经用时 60 秒的活动目标 → 再假装过了 60 秒并调用 indicator → 检查状态栏用量变成“2m”。

调用关系:这个测试针对 GoalStatusState::indicator 的实时补时逻辑。它通过 active_goal_state 造数据,再用断言确认 indicator 会把当前轮次耗时接上。

调用图:外部调用 3 个(now, assert_eq!, active_goal_state)。

tests::active_goal_status_does_not_count_idle_time_before_turn_start197–211 ↗
fn active_goal_status_does_not_count_idle_time_before_turn_start()

作用:验证目标虽然早就被观察到了,但如果真正的活动轮次晚一点才开始,中间空闲时间不会被算进用时。

数据流:测试造出一个已用 60 秒的活动目标,再设定本轮开始时间比观察时间晚 120 秒 → 在本轮开始后 60 秒调用 indicator → 检查结果仍是“2m”,说明只多算了真正活动的 60 秒。

调用关系:这个测试保护 GoalStatusState::indicator 里选择计时起点的细节。它确保函数会用观察时间和轮次开始时间中较晚的那个,避免把等待时间算成工作时间。

调用图:外部调用 4 个(from_secs, now, assert_eq!, active_goal_state)。

tests::active_goal_state213–227 ↗
fn active_goal_state(observed_at: Instant, time_used_seconds: i64) -> GoalStatusState

作用:给测试快速创建一个“进行中目标”的小工具函数。这样多个测试不用重复写一大段目标字段。

数据流:输入是观察时间和已用秒数 → 函数组装一份状态为 Active、没有 token 预算的目标数据 → 调用 GoalStatusState::new 包成状态快照并返回。

调用关系:它只服务于本文件的测试。两个计时相关测试会调用它来准备 GoalStatusState,然后再测试 indicator 的显示结果。

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

tui/src/chatwidget/rate_limits.rs源码 ↗
domain_logicmain loop / request handling / cross-cutting

聊天程序有使用额度限制,如果用户快用完了却没人提醒,就可能突然发不出消息。这个文件就是给 ChatWidget 做“额度提醒员”的:它记录哪些提醒已经说过,避免同一句话反复刷屏;把 5 小时、每天、每周这类额度窗口翻译成人话;收到新的额度快照后,更新状态栏和历史消息;如果用户快到上限,还会建议切到更省额度的模型。它还处理另一类情况:工作区没额度或达到使用限制时,弹出是否通知 owner 的选择框。整体像一个前台接待:后台只给数字和错误码,它负责判断严不严重、该不该打扰用户、以及用什么按钮让用户下一步能继续。

函数细节21
RateLimitWarningState::take_warnings20–73 ↗
fn take_warnings(
        &mut self,
        secondary_used_percent: Option<f64>,
        secondary_window_minutes: Option<i64>,
        primary_used_percent: Option<f64>,
        primary_window_minut

作用:根据当前额度已经用了多少,决定要不要生成新的提醒文字。它会记住之前已经提醒到哪个档位,避免 75%、90%、95% 这些提醒重复出现。

数据流:输入是主额度和次额度的已用百分比,以及它们各自的时间窗口。函数先检查是否已经正好到 100%,到了就不再发“快到了”的提醒;否则逐个看有没有跨过 75、90、95 这些门槛,把最新跨过的门槛变成一句“还剩不到多少”的提示。输出是一组提醒字符串,同时会更新内部索引,表示这些门槛已经提醒过了。

调用关系:它是在 ChatWidget 收到新的额度快照后被用到的。需要写清楚“daily”还是“weekly”这类标签时,它会交给 limit_label_for_window;生成出来的提醒随后会被 ChatWidget 放进聊天历史里显示。

调用图:调用 1 个内部函数(limit_label_for_window);外部调用 3 个(new, format!, matches!)。

limit_label_for_window76–80 ↗
fn limit_label_for_window(window_minutes: Option<i64>, is_secondary: bool) -> String

作用:把一个额度窗口的分钟数变成用户能看懂的标签,比如“daily”或“weekly”。如果看不出来,就用默认名字兜底。

数据流:输入是时间窗口分钟数,以及这是主额度还是次额度。它先尝试把分钟数识别成常见周期;识别成功就输出对应文字,失败就输出“usage”或“secondary usage”这类默认标签。

调用关系:它主要服务于 RateLimitWarningState::take_warnings,让提醒语句不要只说冷冰冰的分钟数,而是说用户更容易理解的额度类型。

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

get_limits_duration82–105 ↗
fn get_limits_duration(windows_minutes: i64) -> Option<String>

作用:识别一个分钟数大概代表什么常见额度周期。比如 1440 分钟大约就是 daily,也就是每天。

数据流:输入是分钟数。函数会先把负数当作 0 处理,然后拿它和 5 小时、1 天、1 周、1 月、1 年这些标准长度比较;如果差距在允许范围内,就输出对应标签,否则输出空值。

调用关系:它是时间窗口翻译流程里的核心判断器。具体“差不多等于”的判断交给 is_approximate_window 来做。

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

fallback_limit_label107–113 ↗
fn fallback_limit_label(is_secondary: bool) -> &'static str

作用:当额度窗口识别不出来时,给提醒文字提供一个保底名称。这样界面不会出现空白或奇怪的技术值。

数据流:输入是一个布尔值,表示是不是次额度。是次额度就输出“secondary usage”,否则输出“usage”。它不改动任何状态。

调用关系:它通常被 limit_label_for_window 用作兜底方案,让上层提醒逻辑总能拿到一段可显示的文字。

is_approximate_window115–119 ↗
fn is_approximate_window(minutes: i64, expected_minutes: i64) -> bool

作用:判断一个分钟数是不是“大约等于”某个标准周期。这里允许 5% 的误差,避免服务器给的数稍有偏差就识别失败。

数据流:输入是实际分钟数和期望分钟数。函数把它们转成小数,然后检查实际值是否落在期望值的 95% 到 105% 之间,最后输出 true 或 false。

调用关系:它被 get_limits_duration 反复调用,用来判断某个窗口是不是 5 小时、一天、一周等常见周期。

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

app_server_rate_limit_error_kind136–147 ↗
fn app_server_rate_limit_error_kind(
    info: &AppServerCodexErrorInfo,
) -> Option<RateLimitErrorKind>

作用:把应用服务器返回的错误信息,归类成界面关心的额度错误类型。这样后面的界面逻辑不用直接理解所有服务器错误细节。

数据流:输入是一条服务器错误信息。函数检查它是不是服务器过载、使用额度超限,或者 HTTP 429 这种“请求太多”的通用限流错误;匹配上就输出对应分类,匹配不上就输出空值。

调用关系:它处在服务器错误和界面反应之间,像翻译员一样把底层错误码翻译成 ChatWidget 可以据此展示提示的类别。

is_app_server_cyber_policy_error149–151 ↗
fn is_app_server_cyber_policy_error(info: &AppServerCodexErrorInfo) -> bool

作用:判断服务器错误是不是网络安全策略相关的拒绝。它把这类错误单独认出来,方便界面走专门提示。

数据流:输入是一条服务器错误信息。函数只检查它是否等于 CyberPolicy,结果输出 true 或 false,不改变任何状态。

调用关系:它和 app_server_rate_limit_error_kind 类似,都是把服务器错误做成更容易判断的小问题,供更上层的聊天界面流程使用。

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

ChatWidget::on_rate_limit_snapshot160–162 ↗
fn on_rate_limit_snapshot(&mut self, snapshot: Option<RateLimitSnapshot>)

作用:处理一次完整的账号额度读取结果。外部拿到额度快照后,会从这里交给聊天界面更新显示。

数据流:输入是可能存在的额度快照。它不自己展开处理,而是标记来源为“账号使用情况读取”,再交给 on_rate_limit_snapshot_from;最终会更新内部额度状态、提醒和状态栏。

调用关系:这是完整额度信息进入 ChatWidget 的入口。真正的合并、提醒和弹窗判断都由 on_rate_limit_snapshot_from 完成。

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

ChatWidget::on_rolling_rate_limit_snapshot164–167 ↗
fn on_rolling_rate_limit_snapshot(&mut self, snapshot: RateLimitSnapshot)

作用:处理服务器陆续推送来的额度更新。它适合那种只包含部分字段的增量通知。

数据流:输入是一条额度快照。函数把它包装成存在的快照,并标记来源为“滚动更新”,然后交给 on_rate_limit_snapshot_from;后者会尽量保留之前已经知道的完整信息。

调用关系:它是零散额度更新进入 ChatWidget 的入口。因为这类更新可能不全,所以后续处理会特别保留旧的元数据。

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

ChatWidget::on_rate_limit_snapshot_from169–284 ↗
fn on_rate_limit_snapshot_from(
        &mut self,
        snapshot: Option<RateLimitSnapshot>,
        source: RateLimitSnapshotSource,
    )

作用:这是本文件最核心的额度更新处理函数。它把新额度快照合并进界面状态,并决定是否显示警告、刷新状态栏、或准备弹出切换模型建议。

数据流:输入是一个可能为空的额度快照,以及快照来源。若有快照,它会补齐缺失的额度、保存计划类型、记录是否达到 Codex 限制、生成一次性警告、判断是否已高用量并需要提示切到低成本模型,最后把整理后的显示数据存起来;如果没有快照,就清空已有额度显示。结果是 ChatWidget 的额度状态、历史消息、重绘请求和状态栏都可能被更新。

调用关系:它被 on_rate_limit_snapshot 和 on_rolling_rate_limit_snapshot 调用,是两种额度信息入口的共同处理站。它会询问 rate_limit_switch_prompt_hidden 看用户是否关闭了提醒,也会把 RateLimitWarningState 生成的警告写入历史。

调用图:调用 1 个内部函数(rate_limit_switch_prompt_hidden);被 2 处调用(on_rate_limit_snapshot, on_rolling_rate_limit_snapshot);外部调用 4 个(now, new_warning_event, matches!, vec!)。

ChatWidget::stop_rate_limit_poller286–286 ↗
fn stop_rate_limit_poller(&mut self)

作用:停止额度轮询器的占位函数。轮询器可以理解为定时去问服务器“额度还剩多少”的小闹钟。

数据流:它没有输入,也没有实际改动;当前实现是空的。调用前后界面状态不变。

调用关系:它被 prefetch_rate_limits 调用,说明预取额度前理论上会先停掉旧轮询。虽然现在没做事,但保留了这个流程位置。

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

ChatWidget::prefetch_rate_limits289–291 ↗
fn prefetch_rate_limits(&mut self)

作用:准备预先读取额度信息。当前版本只会先停止旧的额度轮询,没有真正发起新请求。

数据流:它没有外部输入。执行时调用 stop_rate_limit_poller,结果目前只是保持状态不变。

调用关系:它是“预取额度”流程的入口之一。实际停轮询的动作交给 stop_rate_limit_poller,未来如果要补上真正的预取逻辑,也会放在这个流程里。

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

ChatWidget::should_prefetch_rate_limits294–296 ↗
fn should_prefetch_rate_limits(&self) -> bool

作用:判断当前用户和配置是否需要预先读取额度。只有需要 OpenAI 登录、并且用户确实有 ChatGPT 账号时,才有必要做这件事。

数据流:输入来自 ChatWidget 自己保存的配置和账号状态。函数检查模型提供方是否要求 OpenAI 认证,再检查用户是否有 ChatGPT 账号,最后输出 true 或 false。

调用关系:它通常会被预取流程用来做开关判断,避免在不可能拿到额度或不需要额度的场景里白忙活。

ChatWidget::lower_cost_preset298–304 ↗
fn lower_cost_preset(&self) -> Option<ModelPreset>

作用:在模型列表里找到一个更省额度的推荐模型,也就是 gpt-5.4-mini。找到了才可能弹出“要不要切换”的建议。

数据流:输入来自 ChatWidget 的模型目录。函数尝试读取可用模型列表,寻找既能在选择器里展示、模型名又等于目标低成本模型的项目;找到就复制输出,找不到或读取失败就输出空值。

调用关系:它被 maybe_show_pending_rate_limit_prompt 调用。只有它找到合适模型后,界面才能继续打开切换模型的弹窗。

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

ChatWidget::rate_limit_switch_prompt_hidden306–311 ↗
fn rate_limit_switch_prompt_hidden(&self) -> bool

作用:检查用户是否已经选择隐藏“快到额度上限时建议切换模型”的提醒。

数据流:输入来自配置里的 notices.hide_rate_limit_model_nudge。函数把缺省值当作 false,最后输出这个提醒是否应该被隐藏。

调用关系:它被 on_rate_limit_snapshot_from 和 maybe_show_pending_rate_limit_prompt 使用。前者用它决定要不要准备弹窗,后者用它决定弹窗前是否直接取消。

调用图:被 2 处调用(maybe_show_pending_rate_limit_prompt, on_rate_limit_snapshot_from)。

ChatWidget::maybe_show_pending_rate_limit_prompt313–330 ↗
fn maybe_show_pending_rate_limit_prompt(&mut self)

作用:如果之前已经判断该提醒用户切换到省额度模型,这个函数就真正尝试把弹窗显示出来。

数据流:输入是 ChatWidget 当前保存的提醒状态、配置和模型目录。它先看用户是否隐藏了提醒;再看状态是不是 Pending;如果是,就找低成本模型,找到后打开选择弹窗并把状态改为 Shown,找不到就回到 Idle。

调用关系:它通常在界面循环中被调用,用来把“待显示”的提醒变成真正的弹窗。它会调用 lower_cost_preset 找模型,并把显示工作交给 open_rate_limit_switch_prompt。

调用图:调用 3 个内部函数(lower_cost_preset, open_rate_limit_switch_prompt, rate_limit_switch_prompt_hidden);外部调用 1 个(matches!)。

ChatWidget::open_rate_limit_switch_prompt332–408 ↗
fn open_rate_limit_switch_prompt(&mut self, preset: ModelPreset)

作用:打开一个选择框,问用户要不要切到更省额度的模型。用户可以切换、保持当前模型,或者选择以后再也不提示。

数据流:输入是一个模型预设,里面有模型名、描述和默认推理强度。函数把这些信息组装成三个选项:切换模型、保持当前、保持且以后隐藏;选择切换时会发送更新模型和推理强度的事件,选择隐藏时会发送保存隐藏设置的事件。输出不是返回值,而是在底部面板显示一个选择弹窗。

调用关系:它由 maybe_show_pending_rate_limit_prompt 调用,是额度快满时用户真正看到的交互界面。它通过发送 AppEvent 把用户选择交给应用其他部分执行。

调用图:被 1 处调用(maybe_show_pending_rate_limit_prompt);外部调用 4 个(default, new, format!, vec!)。

ChatWidget::open_workspace_owner_nudge_prompt410–456 ↗
fn open_workspace_owner_nudge_prompt(
        &mut self,
        credit_type: AddCreditsNudgeCreditType,
    )

作用:当工作区没额度或达到使用限制时,弹窗询问用户要不要通知工作区 owner。owner 可以理解为这个团队空间的管理员或付费负责人。

数据流:输入是提醒类型:缺额度,或需要提高使用限制。函数先避免重复发送中的请求;然后根据类型生成标题和提示语,准备“Yes”和“No”两个选项;点 Yes 会发送请求邮件事件,点 No 只关闭弹窗。结果是在底部面板显示确认框。

调用关系:它在用户需要别人帮忙加额度或提限时使用。它不直接发邮件,而是把动作包装成事件,让应用后面的流程去处理发送。

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

ChatWidget::start_add_credits_nudge_email_request458–464 ↗
fn start_add_credits_nudge_email_request(
        &mut self,
        credit_type: AddCreditsNudgeCreditType,
    ) -> bool

作用:标记“通知 owner”的邮件请求已经开始。这样界面可以知道现在有请求在路上,避免重复发。

数据流:输入是请求类型。函数把这个类型存到 add_credits_nudge_email_in_flight 字段里,并返回 true 表示已经开始记录。

调用关系:它配合 open_workspace_owner_nudge_prompt 和 finish_add_credits_nudge_email_request 使用:前者让用户确认,开始函数记录请求中,结束函数再清掉并显示结果。

ChatWidget::finish_add_credits_nudge_email_request466–501 ↗
fn finish_add_credits_nudge_email_request(
        &mut self,
        result: Result<AddCreditsNudgeEmailStatus, String>,
    )

作用:处理通知 owner 邮件请求的结果,并告诉用户成功、冷却中,还是失败了。

数据流:输入是发送结果,可能成功、最近已经发过、或出错。函数先取出之前记录的请求类型,再根据类型和结果挑一句合适的人话消息,加入聊天历史,最后请求界面重绘。

调用关系:它是“通知 owner”流程的收尾。发送请求的异步流程结束后会来到这里,用户就能在历史消息里看到结果。

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

ChatWidget::set_rate_limit_switch_prompt_hidden503–508 ↗
fn set_rate_limit_switch_prompt_hidden(&mut self, hidden: bool)

作用:保存用户是否要隐藏切换低成本模型的额度提醒。用户选择“以后不再显示”时会用到它。

数据流:输入是 hidden 布尔值。函数把它写进配置;如果 hidden 为 true,还会把当前切换提醒状态重置为 Idle,避免已经排队的提醒继续弹出来。

调用关系:它和 open_rate_limit_switch_prompt 里的“never show again”选项配套。其他部分通过事件触发它,从而改变之后额度提醒的行为。

tui/src/chatwidget/review.rs源码 ↗
data_modelreview mode state

这个文件本身不做具体动作,更像是给 ChatWidget 这个聊天界面组件准备的一张“便签纸”。当程序进入代码审查流程时,界面需要记住几件事:当前是否处在审查模式,好让布局和提示横幅跟普通聊天不一样;最近有哪些自动审查请求被用户拒绝,避免反复打扰;进入审查前 token 用量信息是什么,方便退出审查后恢复。这里的 token 可以粗略理解为模型处理文字时消耗的“计量单位”。ReviewState 就把这些信息打包在一起,供聊天界面的其他代码读写。没有它,审查流程的界面状态就会散落在别处,容易出现退出后显示不恢复、提示重复出现之类的问题。

tui/src/chatwidget/session_header.rs源码 ↗
data_modelmain loop

这个文件可以理解成聊天窗口顶部的一张小标签纸,目前只记录一件事:模型名。比如用户正在用某个 AI 模型聊天,界面顶部可能要显示它的名字。SessionHeader 这个结构体就是用来保存这段文字的。创建时通过 new 把模型名放进去;之后如果模型切换了,就用 set_model 更新。set_model 里有个小细节:如果新名字和旧名字一样,它不会重复改动,避免做没必要的更新。这个文件本身不负责把文字画到屏幕上,也不负责选择模型;它只是保存标题栏需要显示的模型信息,像一个专门放“当前模型名”的小盒子。

函数细节2
SessionHeader::new6–8 ↗
fn new(model: String) -> Self

作用:创建一个新的会话标题栏数据对象,并把当前模型名放进去。别人需要初始化聊天界面标题栏时会用它。

数据流:进去的是一个模型名称字符串 → 函数把它存进 SessionHeader 的 model 字段里 → 出来的是一个新的 SessionHeader 对象,里面已经记好了这个模型名。

调用关系:它会被 new_with_op_target 调用,说明在创建更大的聊天界面或会话组件时,会顺手创建这个标题栏数据。它不再把工作交给别的函数,只是完成最基础的初始化。

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

SessionHeader::set_model11–15 ↗
fn set_model(&mut self, model: &str)

作用:更新标题栏里记录的模型名称。模型切换后,界面数据需要跟着变,就可以调用它。

数据流:进去的是一个新的模型名称,以及原来已经存在的 SessionHeader → 函数先比较新旧名字是否一样;如果不一样,就把旧名字替换成新名字;如果一样,就什么也不改 → 结果是这个 SessionHeader 里的模型名保持最新,同时避免无意义的重复写入。

调用关系:它是给外部界面流程在模型名称变化时使用的更新入口。调用图里没有显示谁调用它,但它的作用很明确:当别的部分发现当前模型变了,就通过它把标题栏保存的文字改掉。

tui/src/chatwidget/status_controls.rs源码 ↗
orchestrationcross-cutting

这个文件像聊天界面的“仪表盘控制器”。真正画界面的细节不在这里;这里主要决定什么时候改状态、改成什么、要不要刷新。比如模型在运行、限额刷新完成、用户打开状态栏设置、用户预览终端标题,这些动作都会进到这里。它会把新状态写进 ChatWidget 自己保存的状态里,再通知底部面板重新显示;如果状态也影响终端窗口标题,还会一起刷新标题。它还会处理一些容易出错的情况:异步查 Git 分支或 Git 摘要时,如果结果回来时工作目录已经变了,就丢掉旧结果,避免显示过期信息。另一个重要点是设置界面的“预览”和“确认”分开:预览会临时改配置让用户看效果,取消会恢复原样,确认才真正保留。

函数细节26
ChatWidget::set_status13–54 ↗
fn set_status(
        &mut self,
        header: String,
        details: Option<String>,
        details_capitalization: StatusDetailsCapitalization,
        details_max_lines: usize,
    )

作用:更新聊天界面当前的状态标题和状态详情,比如“正在思考”下面再带一行说明。它保证同一份状态会同时更新到内部状态和底部面板,避免界面不同地方显示不一致。

数据流:进去的是状态标题、可选详情、详情首字母是否要自动大写、最多显示几行。它会先去掉空详情和开头空白,必要时把第一字母大写,然后写入 status_state,并把同样的信息交给 bottom_pane 显示;如果终端标题配置里用到了运行状态,它还会触发状态相关界面刷新。出来没有返回值,但界面状态被改了。

调用关系:这是底层的状态更新入口。ChatWidget::set_status_header 会调用它来做“只改标题、清空详情”的简化操作;它自己还会在需要时把后续刷新交给 refresh_status_surfaces。

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

ChatWidget::set_status_header58–65 ↗
fn set_status_header(&mut self, header: String)

作用:只更新状态标题,并清掉旧的详情文字。适合那些只想显示一句当前状态、不想带额外说明的地方使用。

数据流:进去的是一个标题字符串。它把详情设为 None,把大小写规则和默认行数准备好,然后交给 ChatWidget::set_status;结果是状态栏标题更新,原来的详情不再显示。

调用关系:它是 ChatWidget::set_status 的便捷包装,避免调用者每次都重复填写一堆默认参数。

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

ChatWidget::set_status_line68–70 ↗
fn set_status_line(&mut self, status_line: Option<Line<'static>>)

作用:设置底部状态栏当前要显示的一整行文字。这里的 Line 可以理解成一段已经准备好格式的界面文字。

数据流:进去的是可选的状态栏文字:有值就显示,没有值就清空。它不加工内容,直接交给 bottom_pane 保存和渲染;出来没有返回值,但底部状态栏内容变了。

调用关系:它是 ChatWidget 到底部面板之间的转交口。上层决定状态栏该显示什么,这个函数负责把决定送到底部面板。

ChatWidget::set_active_agent_label81–83 ↗
fn set_active_agent_label(&mut self, active_agent_label: Option<String>)

作用:把当前正在查看或活跃的 agent 标签传给底部栏显示。agent 可以理解成参与工作的某个助手或任务角色。

数据流:进去的是可选标签文字。它不判断谁是活跃 agent,只把 App 已经决定好的标签交给 bottom_pane;结果是底部区域可能显示或隐藏这个标签。

调用关系:这里故意只是传话筒:App 负责判断用户正在看哪个线程,bottom_pane 负责显示,ChatWidget 只把两边接起来。

ChatWidget::refresh_status_line95–97 ↗
fn refresh_status_line(&mut self)

作用:重新计算底部状态栏应该显示什么。配置、会话、Git 信息、运行状态变化后,都需要靠它把状态栏刷新到最新。

数据流:进去没有额外参数,它读取当前配置和运行时状态,然后调用状态界面刷新逻辑。出来没有直接返回值,但状态栏显示会被重新生成。

调用关系:ChatWidget::setup_status_line 在用户确认新状态栏配置后会调用它。这个函数本身是一个门面,把实际刷新交给 refresh_status_surfaces。

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

ChatWidget::cancel_status_line_setup103–105 ↗
fn cancel_status_line_setup(&self)

作用:记录用户取消了状态栏设置。它不会改配置,意思是“什么都不保存,继续用原来的设置”。

数据流:进去没有参数。它只写一条日志,说明用户取消;不改内存配置,也不触发持久化,出来也没有返回值。

调用关系:它通常在设置界面被关闭或取消时被调用。它只调用日志工具 info!,没有把工作继续交给别的状态修改函数。

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

ChatWidget::setup_status_line110–118 ↗
fn setup_status_line(&mut self, items: Vec<StatusLineItem>, use_theme_colors: bool)

作用:把用户在设置界面里选好的状态栏项目应用到当前配置里。比如用户选择显示模型名、Git 分支、剩余上下文等,就在这里生效。

数据流:进去的是用户选择的状态栏项目列表,以及是否使用主题颜色。它把每个项目转成配置里保存的字符串 ID,写入 config.tui_status_line,同时保存颜色选项,然后刷新状态栏;出来没有返回值,但配置和显示都更新了。

调用关系:它是状态栏设置“确认”按钮背后的动作。它会记录日志,然后调用 ChatWidget::refresh_status_line,让刚保存的选择马上出现在界面上。

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

ChatWidget::preview_terminal_title121–129 ↗
fn preview_terminal_title(&mut self, items: Vec<TerminalTitleItem>)

作用:在终端标题设置界面打开时,临时预览用户选择的标题内容。这样用户不必确认保存,就能先看到窗口标题会变成什么样。

数据流:进去的是一组终端标题项目。它第一次预览时会先保存原来的配置快照,然后把新选择转成字符串 ID 写到 config.tui_terminal_title,再刷新终端标题;出来没有返回值,但标题会临时变化。

调用关系:它服务于终端标题设置界面的实时预览。后续如果用户取消,ChatWidget::revert_terminal_title_setup_preview 会用之前保存的快照恢复。

ChatWidget::revert_terminal_title_setup_preview133–140 ↗
fn revert_terminal_title_setup_preview(&mut self)

作用:撤销终端标题设置期间的预览改动,恢复进入设置界面之前的标题配置。

数据流:进去没有参数。它查看是否保存过原始配置:有就取出来写回 config.tui_terminal_title,并刷新终端标题;没有就什么都不做。出来没有返回值。

调用关系:ChatWidget::cancel_terminal_title_setup 会调用它。它是“取消预览不留痕”的关键步骤。

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

ChatWidget::cancel_terminal_title_setup143–146 ↗
fn cancel_terminal_title_setup(&mut self)

作用:处理用户取消终端标题设置的动作。它会丢弃预览效果,让标题回到设置前的样子。

数据流:进去没有参数。它先写日志表示用户取消,然后调用 ChatWidget::revert_terminal_title_setup_preview;结果是临时标题配置被恢复,确认保存没有发生。

调用关系:它是终端标题设置界面取消流程的入口,把真正恢复配置的活交给 ChatWidget::revert_terminal_title_setup_preview。

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

ChatWidget::setup_terminal_title152–158 ↗
fn setup_terminal_title(&mut self, items: Vec<TerminalTitleItem>)

作用:把用户确认的终端标题项目保存到当前配置里,并结束预览会话。

数据流:进去的是用户选中的终端标题项目列表。它把项目转成字符串 ID,清掉“设置前原配置”的临时快照,把新列表写进 config.tui_terminal_title,然后刷新终端标题;出来没有返回值,但新标题配置生效。

调用关系:这是终端标题设置“确认”按钮背后的动作。它会记录日志;确认后再调用恢复预览的函数也不会撤销,因为原始快照已经被丢弃。

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

ChatWidget::set_status_line_branch164–173 ↗
fn set_status_line_branch(&mut self, cwd: PathBuf, branch: Option<String>)

作用:保存异步查到的 Git 分支名,用来显示在状态栏里。Git 分支可以理解成当前代码工作区所在的版本线。

数据流:进去的是查询时对应的工作目录 cwd 和查到的可选分支名。它先检查这个 cwd 是否还是当前状态栏关心的目录:如果不是,就丢掉结果并结束等待;如果是,就保存分支名,标记查询完成,并刷新状态界面。出来没有返回值。

调用关系:它通常在后台 Git 分支查询完成后被调用。它会防止旧目录的结果污染新目录的状态栏,然后把显示刷新交给 refresh_status_surfaces。

ChatWidget::set_status_line_git_summary176–189 ↗
fn set_status_line_git_summary(
        &mut self,
        cwd: PathBuf,
        summary: StatusLineGitSummary,
    )

作用:保存异步查到的 Git 摘要信息,用来在状态栏或相关状态界面显示代码仓库状态。

数据流:进去的是查询时对应的工作目录 cwd 和 Git 摘要。它先确认目录仍然匹配当前状态栏目录;不匹配就丢掉并取消等待,匹配就保存摘要、标记查询完成,然后刷新状态界面。出来没有返回值。

调用关系:它和 ChatWidget::set_status_line_branch 很像,都是后台查询回来后的收口函数。不同的是这里保存的是更完整的 Git 摘要,而不是单个分支名。

ChatWidget::add_status_output191–248 ↗
fn add_status_output(
        &mut self,
        refreshing_rate_limits: bool,
        request_id: Option<u64>,
    )

作用:往聊天历史里加入一块 /status 状态输出,让用户看到当前账号、模型、用量、限额、会话、agent 等信息。它相当于生成一张当前运行情况的小报表。

数据流:进去的是是否正在刷新限额,以及可选请求 ID。它读取 token 用量、协作模式、当前模型、模型默认推理强度、限额快照、agent 摘要、线程信息和当前时间,然后调用状态模块生成一块可显示内容和一个后续可更新的句柄;如果有请求 ID,就保存这个句柄,等限额刷新完成后继续更新;最后把状态内容加入历史。出来没有返回值,但聊天历史多了一条状态输出。

调用关系:它把很多来源的信息汇总起来,交给外部的 compose_agents_summary 和 new_status_output_with_rate_limits_handle 生成最终显示内容。之后 ChatWidget::finish_status_rate_limit_refresh 可以用保存的句柄补上最新限额。

调用图:外部调用 4 个(now, compose_agents_summary, new_status_output_with_rate_limits_handle, default)。

ChatWidget::finish_status_rate_limit_refresh250–275 ↗
fn finish_status_rate_limit_refresh(&mut self, request_id: u64)

作用:当限额刷新请求完成后,更新之前已经插入聊天历史的 /status 输出。这样用户不用再发一次 /status,也能看到最新限额。

数据流:进去的是完成的请求 ID。它读取最新限额快照和当前时间,遍历之前等待刷新的状态输出;匹配这个请求 ID 的就用句柄补写限额信息,不匹配的继续保留等待。若确实更新过内容,就请求界面重画。出来没有返回值,但已有历史内容可能被更新。

调用关系:它接住 ChatWidget::add_status_output 之前保存的更新句柄。它使用 now 获取更新时间,用 with_capacity 准备剩余列表,最后在需要时触发 request_redraw。

调用图:外部调用 2 个(now, with_capacity)。

ChatWidget::open_status_line_setup277–287 ↗
fn open_status_line_setup(&mut self)

作用:打开底部状态栏的设置界面,让用户选择状态栏显示哪些项目、是否使用颜色。

数据流:进去没有参数。它读取当前已配置的状态栏项目、颜色设置、预览数据、事件发送器和按键映射,然后创建 StatusLineSetupView,并交给 bottom_pane 显示。出来没有返回值,但底部区域切换到设置视图。

调用关系:它是打开状态栏设置 UI 的入口。它会调用 ChatWidget::status_surface_preview_data 准备示例数据,再用 StatusLineSetupView::new 创建界面。

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

ChatWidget::open_terminal_title_setup289–299 ↗
fn open_terminal_title_setup(&mut self)

作用:打开终端标题设置界面,让用户选择终端窗口标题里显示哪些信息。

数据流:进去没有参数。它读取当前终端标题配置,先保存一份原始配置快照,再准备预览数据、事件发送器和按键映射,创建 TerminalTitleSetupView 并交给 bottom_pane 显示。出来没有返回值,但底部区域出现标题设置界面。

调用关系:它是终端标题设置 UI 的入口。它会调用 ChatWidget::terminal_title_preview_data 准备更贴近当前真实状态的预览,并用 TerminalTitleSetupView::new 建界面。

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

ChatWidget::status_surface_preview_data301–321 ↗
fn status_surface_preview_data(&mut self) -> StatusSurfacePreviewData

作用:准备状态栏设置界面用的预览数据。用户勾选项目时,需要有样例值可看,不能只看到抽象名字。

数据流:进去没有参数。它遍历所有可预览的状态项目,能从当前运行状态拿到值的就放进预览数据;如果已经有 codex 限额信息,但某些限额项目没有值,它会压掉占位符,避免显示误导性的空样例。出来是一份 StatusSurfacePreviewData。

调用关系:ChatWidget::open_status_line_setup 会直接用它做状态栏设置预览;ChatWidget::terminal_title_preview_data 也会先借用它,再补充终端标题专用的实时值。

调用图:调用 2 个内部函数(from_iter, iter);被 2 处调用(open_status_line_setup, terminal_title_preview_data)。

ChatWidget::terminal_title_preview_data323–336 ↗
fn terminal_title_preview_data(&mut self) -> StatusSurfacePreviewData

作用:准备终端标题设置界面用的预览数据。它在普通状态预览基础上,加入终端标题那些会随时间或运行状态变化的真实值。

数据流:进去没有参数。它先调用 ChatWidget::status_surface_preview_data 得到基础预览,再取当前时间,遍历所有终端标题项目;能映射到预览项目且当前有值的,就写入实时预览值。出来是一份更适合终端标题设置的 StatusSurfacePreviewData。

调用关系:ChatWidget::open_terminal_title_setup 会调用它。它把通用状态预览和终端标题专用取值合在一起。

调用图:调用 1 个内部函数(status_surface_preview_data);被 1 处调用(open_terminal_title_setup);外部调用 2 个(now, iter)。

ChatWidget::status_line_context_window_size338–343 ↗
fn status_line_context_window_size(&self) -> Option<i64>

作用:取当前模型的上下文窗口大小。上下文窗口可以理解成模型一次能记住和处理的文字总容量。

数据流:进去没有参数。它先看 token_info 里有没有模型上下文窗口;没有的话再看配置里的 model_context_window;都没有就返回 None。它不改任何状态。

调用关系:ChatWidget::status_line_context_remaining_percent 会调用它,用窗口大小计算还剩多少容量。

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

ChatWidget::status_line_context_remaining_percent345–360 ↗
fn status_line_context_remaining_percent(&self) -> Option<i64>

作用:计算当前对话还剩多少上下文容量,返回百分比。用户可以据此知道会话是不是快塞满了。

数据流:进去没有参数。它先通过 ChatWidget::status_line_context_window_size 获取总容量;如果拿不到,就认为还剩 100%。拿得到时,它读取最近一次 token 用量,没有用量就用默认 0 用量,然后算剩余百分比并限制在 0 到 100 之间。出来是一个可选整数百分比。

调用关系:ChatWidget::status_line_context_used_percent 会基于它反推出已用百分比。它依赖 TokenUsage 的计算能力,但自己负责选择数据来源和兜底值。

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

ChatWidget::status_line_context_used_percent362–365 ↗
fn status_line_context_used_percent(&self) -> Option<i64>

作用:计算当前对话已经用了多少上下文容量,返回百分比。它是“剩余百分比”的反面说法。

数据流:进去没有参数。它调用 ChatWidget::status_line_context_remaining_percent 取得剩余比例,拿不到就按剩 100% 处理;然后用 100 减掉剩余值,并把结果限制在 0 到 100。出来是一个可选整数百分比。

调用关系:它不重复计算 token 用量,而是复用 ChatWidget::status_line_context_remaining_percent,保证“已用”和“剩余”口径一致。

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

ChatWidget::status_line_total_usage367–372 ↗
fn status_line_total_usage(&self) -> TokenUsage

作用:取当前会话累计的 token 用量。token 可以理解成模型处理文字时使用的计量单位。

数据流:进去没有参数。它查看 token_info 中是否有 total_token_usage;有就复制出来,没有就返回默认的空用量。它不改状态。

调用关系:这是给状态栏或状态输出取用量数据的小工具函数,专门负责提供一个总是可用的累计用量对象。

ChatWidget::status_line_limit_display374–382 ↗
fn status_line_limit_display(
        &self,
        window: Option<&RateLimitWindowDisplay>,
        label: &str,
    ) -> Option<String>

作用:把某个限额窗口转成人能看懂的短文字,比如“5h 80% left”。限额窗口表示某段时间里的使用额度情况。

数据流:进去的是可选限额窗口和标签文字。没有窗口就返回 None;有窗口时,它用 100% 减去已用百分比,限制在 0 到 100,再格式化成“标签 + 剩余百分比 left”的字符串。它不改状态。

调用关系:它是状态栏显示限额时的格式化帮手。内部只用 format! 拼文字,不负责查询限额数据。

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

ChatWidget::status_line_reasoning_effort_label384–391 ↗
fn status_line_reasoning_effort_label(
        effort: Option<&ReasoningEffortConfig>,
    ) -> String

作用:把模型的推理强度设置转成适合显示的文字。推理强度可以理解成模型回答前愿意花多少“思考力”。

数据流:进去的是可选推理强度配置。没有设置,或明确是 None,就返回“default”;其他值就返回配置自身的字符串名字。它不读取也不修改 ChatWidget 状态。

调用关系:这是状态栏或状态输出展示推理强度时的文字转换函数。它独立、简单,不需要调用其他 ChatWidget 方法。

tui/src/status/account.rs源码 ↗
data_modelcross-cutting

这个文件很小,但作用很明确:它给终端界面里的账号状态准备了一个统一说法。这里定义的 StatusAccountDisplay 是一个枚举,也就是“几种可能情况里选一种”的类型。账号显示只有两类:一种是 ChatGpt,可以带上邮箱和套餐信息;另一种是 ApiKey,表示用户是用 API Key(一串用于调用服务的密钥)在使用。邮箱和套餐都写成 Option,意思是“可能有,也可能没有”,这样即使系统暂时拿不到这些信息,界面也不会崩。可以把它理解成前台接待手里的小卡片:卡片只写清楚“这是 ChatGPT 用户,可能有邮箱和套餐”或“这是 API Key 用户”,具体怎么画到屏幕上,则交给别的界面代码去做。

tui/src/status/mod.rs源码 ↗
orchestrationcross-cutting

用户在文字界面里看 /status、底部状态栏、用量限制等信息时,背后需要把系统拿到的原始状态数据,整理成适合人看的文字。这个文件本身不做具体排版,更像一个前台目录牌:告诉 Rust 编译器有哪些子文件属于“状态显示”这一块,并把常用的类型和函数重新导出出去。这样外面的代码不用知道东西藏在 account、card、helpers、rate_limits 这些具体文件里,只要从 status 模块拿就行。它还把测试专用的工具用 #[cfg(test)] 包起来,意思是只在跑测试时开放,正常程序里不会出现。比较重要的是,注释里说明 rate_limits 是状态栏显示用量限制的关键入口,它会把底层的时间窗口数据变成用户本地时间,并判断数据是可用、过期,还是缺失。

tui/src/status/rate_limits.rs源码 ↗
domain_logicstatus rendering

这个文件像一个“翻译和排版员”。服务器会告诉程序:某个使用额度用了多少、什么时候重置、还有多少 credits(可理解为账户点数或额度)。但这些数据原样拿来给人看并不友好,所以这里先把时间戳转成本地时间说明,把百分比、余额、月度限额整理成适合显示的行,再判断这份数据是不是太旧了。它还会处理一些容易误导用户的情况:比如数据超过 15 分钟就标成过期;没有可显示内容就说不可用;非默认的限额桶会在标签里加名字,避免用户不知道是哪一类额度。最后,它还提供进度条和“还剩多少百分比”的短文字,让界面不用到处重复写同样的格式化规则。

函数细节15
RateLimitWindowDisplay::from_window79–91 ↗
fn from_window(window: &RateLimitWindow, captured_at: DateTime<Local>) -> Self

作用:把服务器发来的一个限流窗口,变成界面容易显示的版本。这里的“窗口”就是一段时间内的使用额度,比如 5 小时额度或每日额度。

数据流:进去的是一个原始限流窗口和本次抓取数据的本地时间 → 它读取已用百分比、重置时间、窗口长度,把服务器的 UTC 时间戳转成本地可读的重置说明 → 出来的是一个 RateLimitWindowDisplay,里面有已用百分比、重置文字和窗口分钟数。

调用关系:它是快照转换流程里的小零件,rate_limit_snapshot_display_for_limit 在整理完整快照时会用它分别转换主额度窗口和次额度窗口。它依赖外部时间转换能力来把秒级时间戳变成人能看懂的时间。

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

rate_limit_snapshot_display137–142 ↗
fn rate_limit_snapshot_display(
    snapshot: &RateLimitSnapshot,
    captured_at: DateTime<Local>,
) -> RateLimitSnapshotDisplay

作用:这是测试里用的便捷入口,把一个限流快照转成界面显示快照,并默认把限额名字当成 codex

数据流:进去的是服务器原始快照和抓取时间 → 它补上默认的限额名称 codex → 出来的是整理好的 RateLimitSnapshotDisplay

调用关系:它本身不做复杂工作,只是把活交给 rate_limit_snapshot_display_for_limit。因为带有 cfg(test),它主要服务测试场景,让测试不用每次都手写默认限额名。

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

rate_limit_snapshot_display_for_limit144–166 ↗
fn rate_limit_snapshot_display_for_limit(
    snapshot: &RateLimitSnapshot,
    limit_name: String,
    captured_at: DateTime<Local>,
) -> RateLimitSnapshotDisplay

作用:把某一个指定限额名称下的服务器快照,转换成界面内部使用的显示快照。有人会用它,是因为界面不能直接拿协议数据来画,需要先变成更贴近显示需求的结构。

数据流:进去的是原始快照、限额名称、抓取时间 → 它分别转换主窗口、次窗口、credits 信息和月度花费控制信息 → 出来的是 RateLimitSnapshotDisplay,后续可以继续排成状态行。

调用关系rate_limit_snapshot_display 会调用它。它是“协议数据进入 TUI 显示层”的主要入口之一,内部会把窗口交给 RateLimitWindowDisplay::from_window,把 credits 交给 CreditsSnapshotDisplay::from,把月度限额交给 SpendControlLimitSnapshotDisplay::from_limit

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

CreditsSnapshotDisplay::from169–175 ↗
fn from(value: &CoreCreditsSnapshot) -> Self

作用:把服务器的 credits 原始信息复制成界面需要的 credits 信息。credits 可以理解为账户里的可用点数或额度。

数据流:进去的是服务器给的 credits 快照 → 它取出是否启用 credits、是否无限、余额文本 → 出来的是 CreditsSnapshotDisplay,内容基本保持原意,但类型更适合 TUI 使用。

调用关系rate_limit_snapshot_display_for_limit 在转换完整快照时会用它。它不再继续调用别的内部函数,只负责把 credits 这块数据搬到显示层结构里。

SpendControlLimitSnapshotDisplay::from_limit179–191 ↗
fn from_limit(
        value: &CoreSpendControlLimitSnapshot,
        captured_at: DateTime<Local>,
    ) -> Option<Self>

作用:把工作区花费控制里的月度限额,整理成可以显示的月度额度行。它会顺手把金额和重置时间变成人更容易读的样子。

数据流:进去的是服务器的月度限额快照和抓取时间 → 它把剩余百分比夹在 0 到 100 之间,把已用和总限额格式化成带千分位的数字,并把重置时间戳转成本地时间文字 → 如果金额格式有效,出来一个 SpendControlLimitSnapshotDisplay;如果金额无效,出来 None,表示这条不要显示。

调用关系rate_limit_snapshot_display_for_limit 会在发现快照里有月度限额时调用它。它会把金额格式化工作交给 format_credit_amount,并使用外部时间戳转换函数来处理重置时间。

调用图:调用 1 个内部函数(format_credit_amount);外部调用 2 个(from_timestamp, from)。

compose_rate_limit_data198–206 ↗
fn compose_rate_limit_data(
    snapshot: Option<&RateLimitSnapshotDisplay>,
    now: DateTime<Local>,
) -> StatusRateLimitData

作用:把单个显示快照整理成状态页最终要用的数据状态。它还会处理“根本没有快照”的情况。

数据流:进去的是可选的显示快照和当前时间 → 如果有快照,它把单个快照包装成一组,再交给多快照版本处理;如果没有快照,直接输出 Missing → 出来的是可用、过期、不可用或缺失这几种状态之一。

调用关系:它是单快照场景的方便入口,会被界面初始化或刷新限流数据结束时使用,比如调用图里显示的 newfinish_rate_limit_refresh。真正排列表、判断过期的工作交给 compose_rate_limit_data_many

调用图:调用 1 个内部函数(compose_rate_limit_data_many);被 2 处调用(new, finish_rate_limit_refresh);外部调用 1 个(from_ref)。

compose_rate_limit_data_many208–337 ↗
fn compose_rate_limit_data_many(
    snapshots: &[RateLimitSnapshotDisplay],
    now: DateTime<Local>,
) -> StatusRateLimitData

作用:这是本文件的核心函数:把一个或多个限流显示快照,排成状态页的一行行内容,并判断这些内容是不是已经过期。

数据流:进去的是一组显示快照和当前时间 → 它先看有没有快照,再逐个检查抓取时间是否超过 15 分钟;然后为主额度、次额度、credits、月度信用限额生成显示行;还会给非默认限额加上合适的标签 → 出来的是 AvailableStaleUnavailableMissing,其中可用和过期状态会带着排好的行。

调用关系compose_rate_limit_data、界面创建流程 new、刷新完成流程 finish_rate_limit_refresh 都会用它;两个测试函数也直接调用它验证标签排法。它在生成 credits 行时会把那部分工作交给 credit_status_row

调用图:调用 1 个内部函数(credit_status_row);被 5 处调用(new, finish_rate_limit_refresh, compose_rate_limit_data, non_codex_multi_limit_keeps_group_row, non_codex_single_limit_renders_combined_row);外部调用 11 个(minutes, signed_duration_since, new, with_capacity, format!, is_empty, len, Available, Stale, Text (+1 more))。

render_status_limit_progress_bar343–353 ↗
fn render_status_limit_progress_bar(percent_remaining: f64) -> String

作用:把“还剩多少百分比”画成一个固定宽度的文字进度条。这样状态页可以用一眼能看懂的条形图展示剩余额度。

数据流:进去的是剩余百分比 → 它先把数字限制在 0 到 100 的合理范围,再按 20 格计算该填多少实心块、多少空心块 → 出来的是类似 [████░░░░] 的字符串,不会改动外部数据。

调用关系:它是显示层的小工具,供状态界面渲染额度条时使用。它不依赖本文件其他函数,只使用格式化字符串来拼出结果。

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

format_status_limit_summary356–358 ↗
fn format_status_limit_summary(percent_remaining: f64) -> String

作用:把剩余百分比变成很短的说明文字,比如“42% left”。这种文字适合放在进度条旁边。

数据流:进去的是剩余百分比 → 它四舍五入到没有小数的形式并拼上 left → 出来的是一段摘要字符串。

调用关系:调用图显示它会被 rate_limit_row_lines 使用,也就是在真正把状态行渲染成文本时拿来生成旁边的简短说明。

调用图:被 1 处调用(rate_limit_row_lines);外部调用 1 个(format!)。

credit_status_row364–380 ↗
fn credit_status_row(credits: &CreditsSnapshotDisplay) -> Option<StatusRateLimitRow>

作用:根据 credits 信息决定要不要显示一行“Credits”,以及这一行该显示“Unlimited”还是具体余额。

数据流:进去的是整理过的 credits 信息 → 如果账户没有启用 credits,就不输出;如果是无限 credits,就输出 Credits: Unlimited;否则读取余额,格式化成正整数 credits → 出来的是一个 StatusRateLimitRow,或者 None 表示这块不显示。

调用关系compose_rate_limit_data_many 在排状态行时会调用它。它把余额清洗和取整的细节交给 format_credit_balance,自己负责决定最终这行是否存在以及文本长什么样。

调用图:调用 1 个内部函数(format_credit_balance);被 1 处调用(compose_rate_limit_data_many);外部调用 2 个(format!, Text)。

format_credit_balance382–402 ↗
fn format_credit_balance(raw: &str) -> Option<String>

作用:把后台给的 credits 余额文本,变成适合显示的正整数文本。它会过滤掉空值、零值和无效数字,避免界面显示没意义的余额。

数据流:进去的是一段原始余额字符串 → 它先去掉前后空格,再尝试按整数读取;不行就按小数读取并四舍五入;只有大于 0 的数才接受 → 出来的是数字字符串,或者 None 表示不能显示。

调用关系:它只被 credit_status_row 使用,是 credits 显示链路里的清洗器。这样 credit_status_row 不必关心余额文本里可能有空格、小数或非法内容。

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

format_credit_amount404–410 ↗
fn format_credit_amount(raw: &str) -> Option<String>

作用:把月度限额里的金额文本格式化成更好读的数字,比如给大数字加分隔符。它用于“已用多少 / 总共多少 credits”这种说明。

数据流:进去的是原始金额字符串 → 它去空格后按小数解析,拒绝无穷大、非数字和负数,再四舍五入成整数并加上千分位分隔符 → 出来的是格式化后的金额字符串,或者 None 表示金额不能用。

调用关系SpendControlLimitSnapshotDisplay::from_limit 会调用它两次,分别处理已用金额和总限额。它把最后的分隔符格式化交给外部的 format_with_separators

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

tests::window422–428 ↗
fn window(used_percent: f64) -> RateLimitWindowDisplay

作用:这是测试用的小帮手,快速造一个限流窗口显示对象。它让测试代码不用反复写同样的结构体字段。

数据流:进去的是已用百分比 → 它填上固定的重置文字 soon 和固定的窗口长度 300 分钟 → 出来的是一个 RateLimitWindowDisplay 测试数据。

调用关系:它只服务测试,tests::non_codex_single_limit_renders_combined_row 会用它来构造主限流窗口。它不参与真实运行时的状态显示流程。

tests::non_codex_single_limit_renders_combined_row431–474 ↗
fn non_codex_single_limit_renders_combined_row()

作用:这个测试确认:如果一个非默认限额只有一个窗口,标签会合并成一行,比如 codex-other 5h limit,而不是多插一个分组标题。

数据流:进去的是测试现场临时构造的两个快照,一个默认 codex,一个非默认 codex-other → 它调用 compose_rate_limit_data_many 生成显示行,再取出所有标签和 credits 行数量做比较 → 如果结果不符合预期,测试失败。

调用关系:它直接测试 compose_rate_limit_data_many 的标签组合规则,并用 tests::window 生成测试窗口。它的存在是为了防止以后改排版时把单窗口非默认限额显示得啰嗦或混乱。

调用图:调用 1 个内部函数(compose_rate_limit_data_many);外部调用 4 个(now, assert_eq!, panic!, window)。

tests::non_codex_multi_limit_keeps_group_row477–509 ↗
fn non_codex_multi_limit_keeps_group_row()

作用:这个测试确认:如果一个非默认限额有多个窗口,界面会保留一个分组标题,下面再列主窗口和次窗口。

数据流:进去的是测试构造的一个 codex-other 快照,里面有主窗口和次窗口 → 它调用 compose_rate_limit_data_many 生成行,再检查标签必须依次是分组标题、主额度、次额度 → 如果不一致,测试失败。

调用关系:它直接覆盖 compose_rate_limit_data_many 的多窗口排版规则。和另一个单窗口测试配合起来,保证非默认限额在不同数量窗口下都有清楚的显示方式。

调用图:调用 1 个内部函数(compose_rate_limit_data_many);外部调用 3 个(now, assert_eq!, panic!)。

tui/src/goal_files.rs源码 ↗
domain_logicrequest handling

用户设置目标时,可能会输入一大段文字,粘贴很长的内容,或者附上图片。直接把这些全塞进一条目标消息里,可能超过长度限制,也可能让后端看不到本地图片。这个文件就像一个“打包员”:先检查目标不能为空;把还没展开的粘贴内容和图片找出来;需要时在 app server 那边创建一个 attachments 目录;把大段粘贴写成 txt,把本地图片复制成图片文件;再把原文里的占位符替换成“请读这个文件”的提示。如果最后目标文字还是太长,它会把整个目标写进 goal-objective.md,只留下一个很短的引用句。另一个重要能力是编辑旧目标时,如果目标其实只是指向文件,它会把文件内容读回来给用户改。

函数细节8
materialize_goal_draft33–138 ↗
async fn materialize_goal_draft(
    app_server: &mut AppServerSession,
    codex_home: Option<&GoalFilePath>,
    draft: GoalDraft,
) -> Result<(String, Option<GoalFilePath>)>

作用:把一份还没提交的目标草稿变成最终可提交的目标文字。它会把太大的粘贴内容、本地图片、以及过长的目标正文落到文件里,并返回改写后的目标文字和创建出来的附件目录。

数据流:进去的是 app server 连接、可选的 CODEX_HOME 路径,以及 GoalDraft 草稿。它先检查目标是否为空,再找出正文里正在使用的占位符;遇到粘贴内容就写成 pasted-text-N.txt,遇到本地图片就复制成 image-N.ext;然后把占位符替换成文件提示,并追加没有占位符的图片和远程图片 URL 列表。最后如果目标仍超过允许长度,就把完整目标写成 goal-objective.md,只返回一句“去读这个文件”的引用;出来的是最终目标文字和可能创建的附件目录。

调用关系:它由 set_thread_goal_draft 在用户设置线程目标时调用,是整个“目标草稿变成后端可用目标”的主流程。它把创建目录的事交给 ensure_goal_output_dir,把写文件交给 write_goal_file,把图片后缀判断交给 image_extension,把超长正文的引用句交给 objective_file_reference,还用 append_section 给目标补上图片说明。

调用图:调用 6 个内部函数(expand_pending_pastes, append_section, ensure_goal_output_dir, image_extension, objective_file_reference, write_goal_file);被 1 处调用(set_thread_goal_draft);外部调用 5 个(new, bail!, format!, read, fs_remove_path)。

objective_text_for_edit140–155 ↗
async fn objective_text_for_edit(
    app_server: &mut AppServerSession,
    codex_home: Option<&GoalFilePath>,
    objective: &str,
) -> Result<String>

作用:在用户重新编辑目标时,把可编辑的真实文字拿出来。如果当前目标只是一个“请读某个目标文件”的引用,它会去读那个文件;否则直接返回原文字。

数据流:进去的是 app server 连接、可选的 CODEX_HOME 路径,以及当前目标字符串。它先用 objective_file_path 判断这段文字是不是合法的目标文件引用;如果不是,就原样返回;如果是,就通过 app server 读文件,并把字节转成 UTF-8 文本。出来的是用户编辑器里应该显示的目标正文。

调用关系:它由 open_thread_goal_editor 在打开目标编辑器时调用。它自己不判断复杂路径规则,而是交给 objective_file_path;确认有文件后,再通过 app server 的 fs_read_file_path 把内容取回来。

调用图:调用 1 个内部函数(objective_file_path);被 1 处调用(open_thread_goal_editor);外部调用 2 个(from_utf8, fs_read_file_path)。

objective_file_path157–172 ↗
fn objective_file_path(
    objective: &str,
    codex_home: Option<&GoalFilePath>,
) -> Option<GoalFilePath>

作用:判断一段目标文字是不是本系统生成的“目标文件引用”,如果是,就还原出那个文件路径。这样可以避免把普通文字误当成文件地址去读。

数据流:进去的是目标字符串和 CODEX_HOME 路径。它检查字符串是否正好符合固定格式:前面是“Read the Codex goal objective file at ”,后面是“ before continuing.”;中间必须是绝对路径。然后它确认路径必须长得像 CODEX_HOME/attachments/某个 UUID/goal-objective.md,并且 UUID 真的合法。符合就输出这个路径,不符合就输出空。

调用关系:它被 objective_text_for_edit 调用,用来决定编辑器应该显示原文字还是读取文件内容。它依赖 AppServerPath::from_absolute_str 解析路径,并用 UUID 解析来确认附件目录名不是随便伪造的。

调用图:调用 1 个内部函数(from_absolute_str);被 1 处调用(objective_text_for_edit);外部调用 1 个(parse_str)。

objective_file_reference174–183 ↗
fn objective_file_reference(path: &GoalFilePath) -> Result<String>

作用:为一个已经写好的目标文件生成一条短引用句,让后端知道要先读这个文件。它还会检查这条引用句本身不能超过目标长度限制。

数据流:进去的是目标文件路径。它把路径夹在固定前缀和后缀之间,拼成类似“请先读取这个目标文件”的一句话;然后数字符数。如果连这句引用都太长,就报错;否则返回引用字符串。

调用关系:它被 materialize_goal_draft 在目标正文太长时调用。materialize_goal_draft 先准备好要写入的 goal-objective.md 路径,再请它生成可提交的替代文字;如果生成失败,主流程会清理刚创建的附件目录,避免留下没用的文件。

调用图:被 1 处调用(materialize_goal_draft);外部调用 2 个(bail!, format!)。

ensure_goal_output_dir185–205 ↗
async fn ensure_goal_output_dir(
    app_server: &mut AppServerSession,
    codex_home: Option<&GoalFilePath>,
    output_dir: &mut Option<GoalFilePath>,
) -> Result<GoalFilePath>

作用:确保有一个专门存放本次目标附件的目录。需要写粘贴文本、图片或超长目标文件时,都会先用它拿到安全的输出位置。

数据流:进去的是 app server 连接、可选的 CODEX_HOME,以及一个可被填充的 output_dir。若 output_dir 已经有值,它直接返回,避免重复建目录;否则它要求 CODEX_HOME 必须存在,在 CODEX_HOME/attachments 下生成一个新的 UUID 目录,并通过 app server 创建它。出来的是这个目录路径,同时也把 output_dir 更新为这个路径。

调用关系:它只被 materialize_goal_draft 调用,而且是懒创建:只有真的需要写文件时才建目录。它把实际创建目录的动作交给 app server 的 fs_create_directory_all_path。

调用图:被 1 处调用(materialize_goal_draft);外部调用 2 个(new_v4, fs_create_directory_all_path)。

write_goal_file207–217 ↗
async fn write_goal_file(
    app_server: &mut AppServerSession,
    path: GoalFilePath,
    bytes: Vec<u8>,
) -> Result<()>

作用:把一份目标相关内容写到 app server 能访问的文件路径上。它是这个文件里统一的“写文件出口”。

数据流:进去的是 app server 连接、目标路径和要写入的字节内容。它调用 app server 的写文件接口,把字节写到指定位置;如果失败,就补上一句带路径的错误说明。出来没有正常数据,成功表示文件已经写好,失败则返回错误。

调用关系:它被 materialize_goal_draft 调用,用来写粘贴文本、本地图片副本和超长目标正文。主流程不直接碰底层写文件细节,而是统一经过它,方便错误信息保持一致。

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

append_section218–228 ↗
fn append_section(objective: &mut String, heading: &str, lines: Vec<String>)

作用:给目标文字末尾追加一个小节,比如“引用的图片文件”或“引用的图片网址”。没有内容时它什么也不做。

数据流:进去的是可修改的目标字符串、小节标题和多行内容。它先看内容列表是否为空;不为空时,确保正文和新小节之间有空行,再写入标题和每一行。结果是原目标字符串被原地加上一个清楚的附加说明区。

调用关系:它被 materialize_goal_draft 调用,用来把没有占位符的本地图片、以及远程图片 URL 补充到目标说明里。这样后端即使没在正文里看到图片占位符,也能知道有哪些图片需要参考。

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

image_extension230–242 ↗
fn image_extension(path: &Path) -> String

作用:从本地图片路径里取出一个安全的文件后缀,用来给复制后的图片命名。没有合适后缀时默认用 png。

数据流:进去的是本地图片路径。它读取路径扩展名,只保留英文字母和数字,最多取 8 个字符,避免奇怪符号进入新文件名;如果没有扩展名,或者清理后为空,就输出 png。出来的是一个可用于 image-N.ext 的后缀字符串。

调用关系:它被 materialize_goal_draft 在复制每张本地图片前调用。主流程用它决定附件图片文件名,再把实际图片字节交给 write_goal_file 写入 app server。

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

TUI 转录与历史更新

这些文件将持久化或实时线程项目投射为转录单元格、分隔符、diff 模型,以及用户可见的终端历史更新。

tui/src/app/history_ui.rs源码 ↗
orchestrationmain loop / request handling / clear-screen handling

这个文件像终端聊天界面的“收银台和清洁工”。新消息来了,它把消息放进历史记录,也同步给正在看的转录窗口;如果有用量统计或限速提示,它会等到合适时机再插进去,避免插在 AI 正在输出的半句话中间。用户输入 /clear 或类似清屏操作时,它会先丢掉还没显示的旧行,再清掉屏幕和滚动历史,必要时重新画一个新的会话标题。它还处理两个外部跳转:普通网址用系统浏览器打开;Codex Desktop 会话用 codex:// 链接交给桌面应用打开。这里有一个重要细节:清屏不只是擦屏幕,还会重置内部保存的聊天转录、回放缓冲、回退状态和待显示提示,否则旧内容可能在清屏后又“诈尸”冒出来。

函数细节17
App::insert_history_cell11–36 ↗
fn insert_history_cell(&mut self, tui: &mut tui::Tui, cell: Box<dyn HistoryCell>)

作用:把一块新的历史内容加入聊天记录里,比如一条消息、一张提示卡或一段系统说明。它还会通知界面刷新,让用户能马上看到变化。

数据流:进去的是一个历史单元和终端界面对象。它先把这个单元变成可共享的形式;如果当前打开了转录覆盖层,就也塞一份进去并请求重画;然后放进主历史列表,再按当前终端宽度拆成可显示的行插入界面。最后它提醒聊天组件:如果之前有用量信息在排队,现在可能可以显示了。

调用关系:它是历史内容真正落地的入口之一。App::insert_pending_usage_output 会调用它,把已经准备好的用量或限速提示变成正式历史记录;它自己会向外部的 frame requester 请求刷新画面。

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

App::pending_usage_output_insertion_blocked38–44 ↗
fn pending_usage_output_insertion_blocked(&self) -> bool

作用:判断现在能不能插入用量统计这类提示。它防止这些提示插到正在输出的 AI 消息后面,造成阅读顺序混乱。

数据流:进去的是当前 App 的状态。它查看聊天组件是否说“现在别插”,也检查最后一条历史是不是 AI 消息单元;只要其中一种情况成立,就返回“被挡住了”,否则返回“可以插”。

调用关系App::insert_pending_usage_output_if_ready 会先问它一声。它相当于门卫,确认时机合适后,后面的插入动作才会继续。

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

App::insert_pending_usage_output46–53 ↗
fn insert_pending_usage_output(&mut self, tui: &mut tui::Tui)

作用:把已经算好但还没显示的用量信息或限速重置提示,正式放进历史记录。这样用户能在对话里看到本轮消耗或等待提示。

数据流:进去的是 App 和终端界面。它从聊天组件里取出“已完成的 token 活动输出”和“待显示的限速重置提示”;token 可以理解为模型处理文字的小单位。取到哪个,就包装成历史单元并交给 App::insert_history_cell 显示;取不到就不做事。

调用关系:它不自己判断时机,只负责真正插入。App::insert_pending_usage_output_if_readyApp::insert_pending_usage_output_after_stream_shutdown 会在不同场景下调用它。

调用图:调用 1 个内部函数(insert_history_cell);被 2 处调用(insert_pending_usage_output_after_stream_shutdown, insert_pending_usage_output_if_ready);外部调用 1 个(new)。

App::insert_pending_usage_output_if_ready55–60 ↗
fn insert_pending_usage_output_if_ready(&mut self, tui: &mut tui::Tui)

作用:在普通运行过程中,尝试显示排队中的用量提示。它会先确认不会打断当前聊天内容。

数据流:进去的是 App 和终端界面。它先调用 App::pending_usage_output_insertion_blocked 检查是否被挡住;如果被挡住就直接返回;如果没被挡住,就调用 App::insert_pending_usage_output 把提示插进历史。

调用关系:它是“有空就显示用量提示”的安全入口。它把判断交给 App::pending_usage_output_insertion_blocked,把实际插入交给 App::insert_pending_usage_output

调用图:调用 2 个内部函数(insert_pending_usage_output, pending_usage_output_insertion_blocked)。

App::insert_pending_usage_output_after_stream_shutdown62–67 ↗
fn insert_pending_usage_output_after_stream_shutdown(&mut self, tui: &mut tui::Tui)

作用:在 AI 流式输出结束后,尝试补上用量提示。流式输出就是内容一段一段吐出来,而不是一次性给完。

数据流:进去的是 App 和终端界面。它只检查聊天组件是否还禁止插入用量历史;如果禁止,就停下;如果允许,就调用 App::insert_pending_usage_output 显示排队内容。

调用关系:它用于输出流关闭后的收尾阶段。相比 App::insert_pending_usage_output_if_ready,它不再用最后一条 AI 消息作为额外阻挡条件,因为此时流已经结束。

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

App::open_url_in_browser69–78 ↗
fn open_url_in_browser(&mut self, url: String)

作用:把一个普通网页地址交给系统浏览器打开。成功或失败都会在聊天界面里告诉用户。

数据流:进去的是一个网址字符串。它调用外部浏览器打开函数;如果失败,就把错误写成聊天里的错误消息;如果成功,就写一条“已在浏览器打开”的提示消息。

调用关系:它是用户点击或触发外部链接时的处理点。它把真正打开浏览器的工作交给外部库 webbrowser::open,自己负责把结果反馈到聊天窗口。

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

App::open_desktop_thread80–92 ↗
fn open_desktop_thread(&mut self, thread_id: ThreadId)

作用:把当前会话交给 Codex Desktop 桌面应用打开。这样用户可以从终端切到桌面版继续看同一个线程。

数据流:进去的是线程 ID。它先拼出 codex://threads/... 这种桌面应用专用链接;再调用 open_desktop_thread_url 让操作系统打开;失败时把错误改写成更友好的说明,成功时在聊天里显示“已打开”。

调用关系:它是终端到桌面版的交接入口。它依赖 open_desktop_thread_url 做平台相关的启动动作,失败文案由 desktop_thread_open_error_message 生成。

调用图:调用 2 个内部函数(desktop_thread_open_error_message, open_desktop_thread_url);外部调用 1 个(format!)。

App::clear_ui_header_lines_with_version94–111 ↗
fn clear_ui_header_lines_with_version(
        &self,
        width: u16,
        version: &'static str,
    ) -> Vec<Line<'static>>

作用:生成清屏后要重新显示的会话标题行。标题里会包含当前模型、推理强度、工作目录、版本号等信息。

数据流:进去的是可显示宽度和版本号。它读取聊天组件里的当前模型、服务档位、推理强度,也读取配置里的当前目录和是否是 yolo 模式;然后创建一个会话标题历史单元,并按宽度排成多行文字返回。

调用关系App::clear_ui_header_lines 会调用它并传入默认 CLI 版本。它本身负责把 App 里的状态整理成“清屏后页眉”的显示内容。

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

App::clear_ui_header_lines113–115 ↗
fn clear_ui_header_lines(&self, width: u16) -> Vec<Line<'static>>

作用:用当前程序版本生成清屏后的标题行。调用者不用自己传版本号。

数据流:进去的是显示宽度。它把这个宽度和内置的 Codex CLI 版本号交给 App::clear_ui_header_lines_with_version,拿回排好版的标题行。

调用关系App::queue_clear_ui_header 会调用它。它是一个简化入口,让正常清屏流程不用关心版本参数。

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

App::queue_clear_ui_header117–126 ↗
fn queue_clear_ui_header(&mut self, tui: &mut tui::Tui)

作用:把清屏后的新会话标题插入终端历史显示区。它让用户清屏后仍能看到当前会话的基本信息。

数据流:进去的是 App 和终端界面。它根据当前终端宽度算出历史文字应该多宽,再生成标题行;如果标题不是空的,就插入终端历史行,并标记“已经输出过历史行”。

调用关系App::clear_terminal_ui 在需要重画标题时调用它。它把标题生成交给 App::clear_ui_header_lines,把真正插入屏幕交给终端界面的 insert_history_lines

调用图:调用 1 个内部函数(clear_ui_header_lines);被 1 处调用(clear_terminal_ui);外部调用 1 个(insert_history_lines)。

App::clear_terminal_ui128–159 ↗
fn clear_terminal_ui(
        &mut self,
        tui: &mut tui::Tui,
        redraw_header: bool,
    ) -> Result<()>

作用:执行真正的终端清屏,并可选择重画会话标题。它解决的是“清完屏后不要残留旧内容,也不要让旧队列稍后又显示出来”的问题。

数据流:进去的是 App、终端界面和是否重画标题的开关。它先判断是否在备用屏幕里,清掉尚未插入的历史行;如果在备用屏幕,就清当前可见区域;否则用一条 ANSI 控制序列同时清可见屏幕和滚动历史。之后它把内联视口挪回顶部,重置“已输出历史行”标记;如果要求重画标题,就调用 App::queue_clear_ui_header。最后返回成功或清屏错误。

调用关系:这是 /clear 或类似清屏动作里的核心步骤。它调用终端对象的清理能力,也在最后把重画标题的工作交给 App::queue_clear_ui_header

调用图:调用 1 个内部函数(queue_clear_ui_header);外部调用 2 个(clear_pending_history_lines, is_alt_screen_active)。

App::reset_app_ui_state_after_clear161–163 ↗
fn reset_app_ui_state_after_clear(&mut self)

作用:清屏后重置 App 里和界面显示有关的状态。它避免旧聊天记录、旧提示或旧回放继续影响新画面。

数据流:进去的是 App 自身。它不直接逐项清理,而是把工作交给 App::reset_transcript_state_after_clear;完成后,App 的转录相关状态会回到干净状态。

调用关系:它是清屏后状态重置的外层入口。现在它主要是一个转发点,方便以后如果还有别的 UI 状态要重置,可以放在这里统一安排。

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

App::reset_transcript_state_after_clear165–177 ↗
fn reset_transcript_state_after_clear(&mut self)

作用:把聊天转录和历史显示相关的内部缓存全部清掉。它是防止“清屏后旧内容又回来”的关键。

数据流:进去的是 App 自身。它关闭覆盖层,清空历史单元、延迟显示的行、重排缓存、技能加载警告等;还清掉聊天组件里排队的 token 活动刷新和限速提示,丢弃初始历史回放缓冲,并把回退状态恢复默认。

调用关系App::reset_app_ui_state_after_clear 会调用它。它不负责擦终端屏幕,而是负责擦 App 脑子里记着的旧显示状态。

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

desktop_thread_open_error_message180–184 ↗
fn desktop_thread_open_error_message(err: &str) -> String

作用:生成一条用户看得懂的“打开 Codex Desktop 失败”错误消息。它会顺便提醒用户安装或启动桌面应用。

数据流:进去的是底层失败原因,比如系统命令失败或应用没装。它把原因塞进固定模板里,输出一整句友好的错误说明。

调用关系App::open_desktop_thread 在打开桌面会话失败时调用它。它把偏技术的错误包装成适合显示在聊天界面里的文字。

调用图:被 1 处调用(open_desktop_thread);外部调用 1 个(format!)。

windows_desktop_app_launch_script226–254 ↗
fn windows_desktop_app_launch_script(url: &str) -> String

作用:为 Windows 生成一段 PowerShell 脚本,用来找到并启动 Codex Desktop。PowerShell 可以理解成 Windows 上执行系统管理命令的脚本工具。

数据流:进去的是 codex:// 链接。它先把链接转成 PowerShell 字符串安全写法,再拼出一段脚本:检查 Codex Desktop 包是否安装,检查 exe 和 app.asar 是否存在,最后用这些路径启动桌面应用并传入链接。

调用关系:Windows 版本的 open_desktop_thread_url 会调用它。它只负责“写脚本”,真正执行脚本的是后面的 PowerShell 命令。

调用图:调用 1 个内部函数(powershell_single_quoted_string);被 1 处调用(open_desktop_thread_url);外部调用 1 个(format!)。

powershell_single_quoted_string257–259 ↗
fn powershell_single_quoted_string(value: &str) -> String

作用:把普通字符串变成 PowerShell 单引号字符串,避免里面的单引号把脚本弄坏。

数据流:进去的是任意字符串。它把字符串里的单引号替换成 PowerShell 认可的转义写法,也就是把一个单引号变成两个;然后在外面再包一层单引号,输出安全版本。

调用关系windows_desktop_app_launch_script 在把网址塞进 PowerShell 脚本前会调用它。它是一个小工具,作用是防止脚本文本被特殊字符打断。

调用图:被 1 处调用(windows_desktop_app_launch_script);外部调用 1 个(format!)。

open_desktop_thread_url262–264 ↗
fn open_desktop_thread_url(_url: &str) -> Result<(), String>

作用:按当前操作系统尝试打开 Codex Desktop 专用链接。不同系统打开应用的办法不一样,所以这里把差异藏起来。

数据流:进去的是 codex:// 链接。在 macOS 上,它用系统的 open 命令打开;在 Windows 上,它生成并运行 PowerShell 脚本来启动 Codex Desktop;在其他系统上,它直接返回“不支持”的错误。出来的是成功结果,或者一段说明失败原因的字符串。

调用关系App::open_desktop_thread 会调用它完成真正的桌面应用启动。Windows 路径下,它会借助 windows_desktop_app_launch_script 生成脚本,并可能把 PowerShell 输出的错误转成返回值。

调用图:调用 1 个内部函数(windows_desktop_app_launch_script);被 1 处调用(open_desktop_thread);外部调用 3 个(from_utf8_lossy, new, format!)。

tui/src/chatwidget/tool_lifecycle.rs源码 ↗
orchestration对话进行中,收到工具事件和协作事件时

这个文件是 ChatWidget 的一组事件处理器。ChatWidget 可以理解成终端里的聊天窗口,而这些函数负责把“工具事件”变成聊天记录里的可见卡片。比如开始改文件时加一条补丁记录,查看图片时插入图片调用记录,网页搜索开始时先显示一个会动的“搜索中”卡片,结束后再把它补全。它还处理 MCP 工具调用,MCP 可以粗略理解成“外部工具服务器接口”;开始时显示正在调用哪个工具,完成时把结果或错误写进记录。这里还有一个重要点:有些事件可能要先排队,等当前显示状态合适时再处理,避免聊天流、活动卡片和最终历史记录互相打架。

函数细节17
ChatWidget::on_patch_apply_begin9–12 ↗
fn on_patch_apply_begin(&mut self, changes: HashMap<PathBuf, FileChange>)

作用:在系统准备把补丁应用到文件时,把这件事显示到聊天历史里。这样用户能马上知道:接下来不是普通回答,而是在改项目文件。

数据流:进去的是一组文件改动和当前工作目录信息 → 它先记录本轮对话有可见活动,再用这些改动生成一条“补丁事件”历史记录 → 出来的是聊天记录里多了一块说明哪些文件会被改。

调用关系:它在补丁开始时被触发,把具体展示工作交给 new_patch_event 生成历史卡片,然后由 ChatWidget 放进聊天历史。

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

ChatWidget::on_view_image_tool_call14–22 ↗
fn on_view_image_tool_call(&mut self, path: AbsolutePathBuf)

作用:当工具要查看一张图片时,把这次查看图片的动作插入聊天记录。这样用户知道模型为什么突然根据某张图片继续说话。

数据流:进去的是图片的绝对路径 → 它记录活动、先把正在流式输出的回答收尾并加分隔,再生成一条“查看图片”的历史记录 → 出来的是界面新增一条图片查看记录,并请求刷新屏幕。

调用关系:它在查看图片工具被调用时运行,展示内容由 new_view_image_tool_call 创建,最后请求界面重画,让用户立即看到。

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

ChatWidget::on_image_generation_begin24–27 ↗
fn on_image_generation_begin(&mut self)

作用:在图片生成开始时,为后续图片生成记录腾出干净位置。它不直接添加结果,因为这时图片还没生成完。

数据流:进去没有额外数据 → 它记录本轮有可见活动,并把当前正在输出的回答先收尾 → 出来的是界面状态被整理好,等待图片生成完成事件。

调用关系:它是图片生成流程的开头,后面的 ChatWidget::on_image_generation_end 会接着把最终调用信息写进历史。

ChatWidget::on_image_generation_end29–44 ↗
fn on_image_generation_end(
        &mut self,
        call_id: String,
        status: String,
        revised_prompt: Option<String>,
        saved_path: Option<AbsolutePathBuf>,
    )

作用:在图片生成结束后,把这次生成图片的结果写进聊天记录,包括调用编号、状态、改写后的提示词和保存位置。

数据流:进去的是调用编号、状态、可能有的改写提示词、可能有的图片保存路径 → 它先收尾当前回答流,再生成一条图片生成记录 → 出来的是聊天历史多了一条生成图片的结果,并刷新界面。

调用关系:它接在图片生成开始之后使用,把具体卡片创建交给 new_image_generation_call,然后通知界面重画。

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

ChatWidget::on_file_change_completed46–52 ↗
fn on_file_change_completed(&mut self, item: ThreadItem)

作用:在文件改动完成时,决定现在处理,还是先放进队列等一等。这样可以避免多个工具事件同时更新界面时把显示顺序弄乱。

数据流:进去的是一个线程事件 ThreadItem,里面应该描述文件改动完成情况 → 它复制一份事件,一份用于排队,一份用于立即处理 → 出来的是事件要么进入队列,要么交给真正的完成处理函数。

调用关系:它是文件改动完成事件的入口;真正判断成功失败并写历史的是 ChatWidget::handle_file_change_completed_now。

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

ChatWidget::on_mcp_tool_call_started54–60 ↗
fn on_mcp_tool_call_started(&mut self, item: ThreadItem)

作用:在 MCP 外部工具调用开始时,决定立刻显示“工具运行中”,还是先排队。MCP 可以理解成让模型去调用另一个工具服务。

数据流:进去的是一个 ThreadItem 工具调用事件 → 它复制事件,交给统一的延后或立即处理机制 → 出来的是事件被排队,或者马上进入开始显示流程。

调用关系:它是 MCP 调用开始事件的入口;如果可以立即处理,会走 ChatWidget::handle_mcp_tool_call_started_now。

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

ChatWidget::on_mcp_tool_call_completed62–68 ↗
fn on_mcp_tool_call_completed(&mut self, item: ThreadItem)

作用:在 MCP 外部工具调用结束时,决定立刻把结果显示出来,还是先排队等待合适时机。这样可以保持界面里的“开始”和“结束”顺序合理。

数据流:进去的是一个 ThreadItem 工具完成事件 → 它复制事件,分别准备给队列和立即处理路径使用 → 出来的是事件被保存起来,或者马上进入结果展示流程。

调用关系:它是 MCP 调用完成事件的入口;真正把结果或错误写进活动卡片的是 ChatWidget::handle_mcp_tool_call_completed_now。

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

ChatWidget::on_web_search_begin70–81 ↗
fn on_web_search_begin(&mut self, call_id: String)

作用:在网页搜索开始时,在聊天界面放一个“搜索中”的临时卡片。用户可以立刻知道系统正在联网查资料,而不是卡住了。

数据流:进去的是搜索调用编号 → 它记录活动、收尾当前回答、清掉旧的活动卡片,再创建新的网页搜索活动卡片 → 出来的是 transcript 里有一个 active_cell,也就是当前正在进行的显示卡片,并触发刷新。

调用关系:它是网页搜索流程的开头,使用 new_active_web_search_call 创建临时卡片;ChatWidget::on_web_search_end 会在搜索结束时补全或替换这张卡片。

调用图:外部调用 3 个(new, new, new_active_web_search_call)。

ChatWidget::on_web_search_end83–109 ↗
fn on_web_search_end(
        &mut self,
        call_id: String,
        query: String,
        action: codex_app_server_protocol::WebSearchAction,
    )

作用:在网页搜索结束时,把“搜索中”的临时卡片改成完成状态;如果找不到对应临时卡片,就直接补一条完成记录。

数据流:进去的是调用编号、搜索词和搜索动作结果 → 它先收尾当前回答,然后查当前活动卡片是不是同一次搜索;是的话更新内容、标记完成并落到历史里,不是的话新建一条网页搜索历史 → 出来的是聊天记录里有完整的搜索信息,并标记本轮确实做过工作。

调用关系:它接在 ChatWidget::on_web_search_begin 后面;正常情况下补全 WebSearchCell,异常或错过开始事件时用 new_web_search_call 兜底创建历史记录。

调用图:外部调用 2 个(clone, new_web_search_call)。

ChatWidget::on_collab_event111–115 ↗
fn on_collab_event(&mut self, cell: PlainHistoryCell)

作用:把协作相关的普通历史卡片加入聊天记录。这里的协作可以理解成其他代理或子任务在旁边帮忙干活。

数据流:进去的是一张已经做好的 PlainHistoryCell 历史卡片 → 它先把当前回答流收尾,再把卡片加入历史 → 出来的是聊天界面多了一条协作事件,并请求刷新。

调用关系:它是协作事件的通用落点,被 ChatWidget::on_collab_agent_tool_call 和 ChatWidget::on_sub_agent_activity 调用,用来统一插入历史记录。

调用图:被 2 处调用(on_collab_agent_tool_call, on_sub_agent_activity)。

ChatWidget::on_collab_agent_tool_call117–147 ↗
fn on_collab_agent_tool_call(&mut self, item: ThreadItem)

作用:处理协作代理的工具调用,比如启动一个新代理或汇报工具进度,并把它转换成用户看得懂的历史卡片。

数据流:进去的是一个 ThreadItem → 它先确认这确实是协作代理工具调用,再根据工具类型和状态缓存或取出“启动代理”的摘要,最后尝试生成历史卡片 → 出来的是可能新增一条协作工具调用记录,也可能因为事件类型不对而什么都不做。

调用关系:它会调用 spawn_request_summary 提取启动代理摘要,调用 tool_call_history_cell 生成展示卡片;生成成功后交给 ChatWidget::on_collab_event 统一写入聊天历史。

调用图:调用 3 个内部函数(on_collab_event, spawn_request_summary, tool_call_history_cell);外部调用 1 个(matches!)。

ChatWidget::on_sub_agent_activity149–154 ↗
fn on_sub_agent_activity(&mut self, item: ThreadItem)

作用:处理子代理的活动汇报,并把有价值的活动变成聊天记录。这样用户能看到后台小助手做了什么。

数据流:进去的是一个 ThreadItem → 它记录本轮有可见活动,然后尝试从事件里做出一张子代理活动卡片 → 出来的是如果能生成卡片,就加入聊天历史;不能生成就不改变界面。

调用关系:它调用 sub_agent_activity_history_cell 负责把事件翻译成卡片,再交给 ChatWidget::on_collab_event 做统一插入和刷新。

调用图:调用 2 个内部函数(on_collab_event, sub_agent_activity_history_cell)。

ChatWidget::handle_file_change_completed_now156–167 ↗
fn handle_file_change_completed_now(&mut self, item: ThreadItem)

作用:真正处理“文件改动已经完成”的事件。成功时通常不多说,因为前面已有“已编辑”的记录;失败时补一条失败提示。

数据流:进去的是一个 ThreadItem → 它只接受 FileChange 类型,读取里面的状态;如果状态是失败,就添加一条补丁应用失败记录 → 出来的是聊天历史可能多一条失败提示,并且 transcript 被标记为本轮做过实际工作。

调用关系:它由 ChatWidget::handle_queued_item_completed_now 在处理队列完成事件时调用,也会被文件完成入口的立即处理路径使用。

调用图:被 1 处调用(handle_queued_item_completed_now);外部调用 3 个(new, new_patch_apply_failure, matches!)。

ChatWidget::handle_mcp_tool_call_started_now169–194 ↗
fn handle_mcp_tool_call_started_now(&mut self, item: ThreadItem)

作用:真正显示 MCP 工具调用开始的状态。它会在界面上放一张“某个服务器上的某个工具正在运行”的活动卡片。

数据流:进去的是一个 ThreadItem → 它只接受 McpToolCall 类型,取出调用编号、服务器、工具名和参数;然后收尾当前回答、清掉旧活动卡片、创建新的 MCP 活动卡片 → 出来的是 transcript 的 active_cell 变成这次工具调用,并刷新界面。

调用关系:它由 ChatWidget::handle_queued_item_started_now 调用,也可能从开始事件入口直接触发;展示卡片由 new_active_mcp_tool_call 创建。

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

ChatWidget::handle_mcp_tool_call_completed_now196–255 ↗
fn handle_mcp_tool_call_completed_now(&mut self, item: ThreadItem)

作用:真正处理 MCP 工具调用结束,把成功结果或错误信息显示出来,并结束“运行中”的活动卡片。

数据流:进去的是一个 McpToolCall 类型的 ThreadItem → 它取出调用信息、结果、错误和耗时,把毫秒转换成时间长度;有错误就做成失败结果,有正常结果就整理成工具结果,没有结果也没有错误就生成一条内部错误 → 出来的是对应活动卡片被补全并落入历史,必要时还会追加额外历史卡片,同时标记本轮做过实际工作。

调用关系:它由 ChatWidget::handle_queued_item_completed_now 调用,也可由完成事件入口立即触发;如果当前 active_cell 正好是同一个调用,就补全它,否则会先新建对应 MCP 卡片再补全,保证历史不丢。

调用图:被 1 处调用(handle_queued_item_completed_now);外部调用 3 个(new, from_millis, new_active_mcp_tool_call)。

ChatWidget::handle_queued_item_started_now257–267 ↗
fn handle_queued_item_started_now(&mut self, item: ThreadItem)

作用:处理队列里“某件事开始了”的事件,并按事件类型交给对应处理函数。它像一个分拣员,把不同包裹送到不同窗口。

数据流:进去的是一个 ThreadItem → 它查看事件类型;如果是命令执行,就交给命令开始处理;如果是 MCP 工具调用,就交给 MCP 开始处理;其他类型忽略 → 出来的是对应的界面状态被更新,或没有变化。

调用关系:它是排队机制释放“开始事件”时的分发点;在这个文件里它会把 MCP 事件交给 ChatWidget::handle_mcp_tool_call_started_now。

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

ChatWidget::handle_queued_item_completed_now269–278 ↗
fn handle_queued_item_completed_now(&mut self, item: ThreadItem)

作用:处理队列里“某件事完成了”的事件,并按类型分发给真正的完成处理函数。这样不同工具的结束展示能各走各的规则。

数据流:进去的是一个 ThreadItem → 它判断这是命令完成、文件改动完成,还是 MCP 工具完成;然后交给对应函数 → 出来的是聊天历史或活动卡片被更新,未知类型则不处理。

调用关系:它是排队机制释放“完成事件”时的分发点;在这个文件里它会调用 ChatWidget::handle_file_change_completed_now 和 ChatWidget::handle_mcp_tool_call_completed_now。

调用图:调用 2 个内部函数(handle_file_change_completed_now, handle_mcp_tool_call_completed_now)。

tui/src/diff_model.rs源码 ↗
data_modelcross-cutting

这个文件没有复杂动作,主要是在规定一种数据长什么样。可以把它想成快递单:真正的包裹是文件内容和差异文本,而这张单子告诉界面“这是新增文件、删除文件,还是修改文件”。核心类型是 FileChange,它有三种情况:Add 表示新加了一个文件,并带上完整内容;Delete 表示删掉一个文件,也保留被删前的内容,方便预览;Update 表示改了已有文件,保存的是 unified diff(一种用加号、减号展示改动的文本格式),还可以带 move_path,表示这个文件可能被移动或改名。它还支持 Serialize 和 Deserialize,也就是能在程序内部数据和可保存、可传输的格式之间互相转换。这样 TUI(终端用户界面)不用猜文件发生了什么,只要按这个模型显示即可。

tui/src/history_cell/separators.rs源码 ↗
domain_logicmain loop / history rendering / task completion

在文字界面(TUI,也就是终端里的图形界面)里,聊天记录如果一轮接一轮堆在一起,会很难分清边界。这个文件就像在笔记本不同章节之间画横线:如果助手这轮跑了命令、调了工具、请求了模型接口,就显示一条分隔线;如果还有统计数据,就把“用了多久”“本地工具调用几次”“模型推理多久”“WebSocket 收发事件多久”等信息拼成简短标签。它还有一个小心思:少于等于 60 秒的工作时间不会显示“Worked for”,避免界面被不重要的信息刷屏。核心结构是 FinalMessageSeparator,负责生成屏幕上看到的线;runtime_metrics_label 把一堆数字统计翻译成人能读懂的一句话。

函数细节6
FinalMessageSeparator::new17–25 ↗
fn new(
        elapsed_seconds: Option<u64>,
        runtime_metrics: Option<RuntimeMetricsSummary>,
    ) -> Self

作用:创建一个“最终消息分隔线”对象。别的地方在一轮助手工作结束时,用它把耗时和统计数据打包起来,等界面渲染历史记录时显示。

数据流:进去的是可选的总耗时秒数,以及可选的运行统计摘要;它不做复杂计算,只把这两份信息保存到结构里;出来的是一个 FinalMessageSeparator,之后可以用来生成可显示的分隔线或原始文本。

调用关系:当流式回复过程中需要插入分隔线、任务完成时收尾,或者测试检查分隔线行为时,会调用它。它是这个小组件的入口,后面的 display_lines 和 raw_lines 都靠它保存下来的数据来决定显示什么。

调用图:被 4 处调用(handle_streaming_delta, on_task_complete, final_message_separator_hides_short_worked_label_and_includes_runtime_metrics, final_message_separator_includes_worked_label_after_one_minute)。

FinalMessageSeparator::display_lines28–54 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:生成真正要画到终端聊天历史里的分隔线。它会根据屏幕宽度,把横线和标签排成一行,像“── Worked for 1m • Inference: 2 calls (3.4s) ──”这样的效果。

数据流:进去的是当前可用宽度,以及对象里保存的耗时和统计摘要;它先决定要不要显示“Worked for”,再把运行统计变成标签,最后按宽度截断或补齐横线;出来的是一组可渲染的行,并且不会改动原对象。

调用关系:它位于界面渲染阶段:历史记录要显示到终端时会用到它。它会借助 runtime_metrics_label 把统计数字变成文字,也会使用外部的格式化和行构造工具,把普通字符串变成终端能显示的样式行。

调用图:外部调用 3 个(new, format!, vec!)。

FinalMessageSeparator::raw_lines56–73 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:生成不带横线装饰的纯文本统计内容。这个通常用于复制、日志、测试或需要“原始内容”而不是屏幕样式的地方。

数据流:进去的是对象里保存的耗时和运行统计;它用和显示版本类似的规则收集标签,但如果没有任何有意义标签,就返回空列表;出来的是纯文字行,不包含为了填满屏幕宽度而画的长横线。

调用关系:它和 display_lines 是同一份信息的两种出口:display_lines 给人看界面,raw_lines 给需要纯内容的流程用。它同样会把统计摘要交给 runtime_metrics_label 来翻译成人话。

调用图:外部调用 3 个(new, format!, vec!)。

runtime_metrics_label76–158 ↗
fn runtime_metrics_label(summary: RuntimeMetricsSummary) -> Option<String>

作用:把机器统计出来的一堆数字,整理成用户能读懂的一段运行摘要。比如本地工具调用了几次、模型接口用了多久、流式事件来了多少个。

数据流:进去的是 RuntimeMetricsSummary,也就是一次运行的汇总统计;它逐项检查哪些计数或耗时大于 0,只把真的发生过的项目加入文字里,并把毫秒格式化成 ms 或 s;出来的是一个可选字符串,如果完全没有统计内容就返回空。

调用关系:FinalMessageSeparator::display_lines 和 FinalMessageSeparator::raw_lines 会依赖它生成标签。它把细小工作交给 format_duration_ms 处理时间显示,交给 pluralize 决定英文单复数,从而让最终文字既紧凑又不别扭。

调用图:调用 2 个内部函数(format_duration_ms, pluralize);外部调用 2 个(new, format!)。

format_duration_ms160–167 ↗
fn format_duration_ms(duration_ms: u64) -> String

作用:把毫秒数变成人更容易看的时间文字。小于 1 秒就显示“123ms”,达到 1 秒就显示类似“1.2s”。

数据流:进去的是一个毫秒数;它判断是否至少 1000 毫秒,够 1 秒就换算成秒并保留一位小数,否则保留毫秒;出来的是格式化好的字符串。

调用关系:runtime_metrics_label 在整理每项耗时时会调用它。它是一个小工具,专门保证所有统计项的时间写法一致。

调用图:被 1 处调用(runtime_metrics_label);外部调用 1 个(format!)。

pluralize169–171 ↗
fn pluralize(count: u64, singular: &'static str, plural: &'static str) -> &'static str

作用:根据数量选择英文单数或复数词。比如 1 次用“call”,2 次以上用“calls”。

数据流:进去的是数量、单数词和复数词;它只判断数量是不是 1;出来的是应该使用的那个词,不改动任何外部状态。

调用关系:runtime_metrics_label 在拼接“call/calls”“event/events”“Stream/Streams”这类标签时会调用它。它让统计文字读起来更自然,不会出现“1 calls”这种别扭表达。

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

tui/src/insert_history.rs源码 ↗
io_transport聊天消息定稿后写入终端历史区时

Codex 的聊天界面有一块正在刷新的视口,但已经结束的消息不能一直靠界面重画保存,否则用户用终端滚轮往上翻时可能看不到。这个文件做的事,就像把旧聊天记录“塞到屏幕上方的纸卷里”。它会先判断每行要不要提前换行:普通文字按宽度排好,网址尽量不硬切开,避免链接失效。然后它用 ANSI 转义序列(给终端看的控制指令)设置滚动区域、移动光标、写入带颜色和样式的文字,再把光标放回原处。它还专门照顾 Zellij 这类终端复用器的特殊行为,防止长行软换行后跑进正在编辑的区域。

函数细节30
insert_history_lines61–69 ↗
fn insert_history_lines(
    terminal: &mut crate::custom_terminal::Terminal<B>,
    lines: Vec<Line>,
) -> io::Result<()>

作用:这是最常用的入口:把一组普通的界面文字行插入终端历史。调用者不用关心换行策略,默认会先帮文字按终端宽度整理好。

数据流:输入是一台终端对象和若干行文字 → 它直接选用默认的“预先换行”策略 → 返回写入是否成功,并可能让终端历史区多出这些行。

调用关系:很多界面快照和错误展示流程会从这里开始。它不自己干复杂活,而是把任务交给 insert_history_lines_with_wrap_policy。

调用图:调用 1 个内部函数(insert_history_lines_with_wrap_policy);被 22 处调用(thread_goal_ephemeral_error_message_renders_snapshot, chained_config_error_wraps_in_history_snapshot, app_server_guardian_review_denied_renders_denied_request_snapshot, app_server_guardian_review_timed_out_renders_timed_out_request_snapshot, guardian_approved_exec_renders_approved_request, guardian_approved_request_permissions_renders_request_summary, guardian_denied_exec_renders_warning_and_denied_request, guardian_timed_out_exec_renders_warning_and_timed_out_request, app_server_mcp_startup_failure_renders_warning_history, chatwidget_exec_and_status_layout_vt100_snapshot (+12 more))。

insert_history_lines_with_wrap_policy71–85 ↗
fn insert_history_lines_with_wrap_policy(
    terminal: &mut crate::custom_terminal::Terminal<B>,
    lines: Vec<Line>,
    wrap_policy: HistoryLineWrapPolicy,
) -> io::Result<()>

作用:这个函数允许调用者明确选择长行怎么换行。比如有时希望 Codex 先按单词换好,有时希望交给终端自己软换行。

数据流:输入是终端、文字行和换行策略 → 它补上标准插入模式 → 把工作继续交给更底层的插入函数,输出写入结果。

调用关系:它位于简单入口和底层实现之间。insert_history_lines 会调用它,测试也会用它检查“交给终端换行”的情况。

调用图:调用 1 个内部函数(insert_history_lines_with_mode_and_wrap_policy);被 2 处调用(insert_history_lines, vt100_terminal_wrap_policy_does_not_pre_wrap_long_paragraph)。

insert_history_lines_with_mode_and_wrap_policy87–102 ↗
fn insert_history_lines_with_mode_and_wrap_policy(
    terminal: &mut crate::custom_terminal::Terminal<B>,
    lines: Vec<Line>,
    mode: InsertHistoryMode,
    wrap_policy: HistoryLineWrapPolicy,
)

作用:这个函数同时允许选择“怎么插入”和“怎么换行”。它主要给需要特殊终端行为的地方用,例如 Zellij 原始写入模式。

数据流:输入是普通文字行、插入模式和换行策略 → 它先把普通行转换成带链接信息的行 → 再交给真正执行终端写入的函数,输出成功或错误。

调用关系:它接住上层的普通文本请求,并调用 plain_hyperlink_lines 补齐链接结构,然后交给 insert_history_hyperlink_lines_with_mode_and_wrap_policy。Zellij 相关测试会直接走到这里。

调用图:调用 2 个内部函数(insert_history_hyperlink_lines_with_mode_and_wrap_policy, plain_hyperlink_lines);被 3 处调用(insert_history_lines_with_wrap_policy, vt100_zellij_raw_insert_keeps_soft_wrapped_tail_above_viewport, vt100_zellij_raw_replay_keeps_overflowing_soft_wrapped_tail_above_viewport)。

leading_whitespace_prefix258–277 ↗
fn leading_whitespace_prefix(line: &Line<'_>) -> Line<'static>

作用:这个函数取出一行开头的空白缩进,比如几个空格或制表符。它用于让长段落换行后,后续行还能保持原来的缩进。

数据流:输入是一行带样式的文字 → 它从前往后找每个片段里连续的空白字符,并保留这些空白原本的样式 → 输出一行只包含前导空白的新行。

调用关系:插入历史时,核心函数会用它给自动换行设置“后续行缩进”。显示超链接行的流程也会用它,保证带前缀的链接或代码块换行后不乱掉。

调用图:被 2 处调用(display_hyperlink_lines, insert_history_hyperlink_lines_with_mode_and_wrap_policy);外部调用 3 个(from, styled, new)。

write_history_line282–329 ↗
fn write_history_line(
    writer: &mut W,
    line: &HyperlinkLine,
    wrap_width: usize,
) -> io::Result<()>

作用:这个函数负责把单独一行历史写到终端上,包括清理残留内容、设置颜色样式、写文字和超链接标记。

数据流:输入是写入器、一行带链接信息的文字和终端宽度 → 它先计算这行会软换成几行,清掉后续可能残留的屏幕行,再合并整行样式和每段样式,最后写出带 ANSI 控制码的文字 → 输出写入成功或失败。

调用关系:核心插入函数每遇到一行都会调用它。它自己不决定光标位置,只负责当前位置上的这一行;真正写每个文字片段的活交给 write_spans。

调用图:调用 3 个内部函数(write_spans, width, decorate_spans);被 2 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy, writes_semantic_web_link_without_changing_visible_text);外部调用 2 个(from, queue!)。

SetScrollRegion::write_ansi335–337 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result

作用:这个函数生成“设置终端滚动区域”的 ANSI 指令。滚动区域就像告诉终端:只有这几行可以被往上或往下推。

数据流:输入是一个起止行范围和一个字符串写入目标 → 它把范围写成终端认识的转义序列 → 输出格式化是否成功。

调用关系:标准插入模式会通过 crossterm 调用它。它配合 ResetScrollRegion 使用,避免插入历史时把输入框区域也一起滚走。

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

SetScrollRegion::execute_winapi340–342 ↗
fn execute_winapi(&self) -> std::io::Result<()>

作用:这个函数明确禁止用 Windows WinAPI 方式执行设置滚动区域。这里要求走 ANSI 指令路径,避免行为不一致。

数据流:输入是命令本身 → 一旦被调用就直接报错崩溃 → 不会正常产出终端效果。

调用关系:它是 crossterm 命令接口在 Windows 下需要提供的分支,但本文件不希望这条路被使用,所以用 panic 提醒开发者。

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

SetScrollRegion::is_ansi_code_supported345–348 ↗
fn is_ansi_code_supported(&self) -> bool

作用:这个函数告诉 crossterm:这个命令可以用 ANSI 转义序列表示。简单说,就是“请按终端控制码来发”。

数据流:没有额外输入 → 固定返回 true → 让调用方认为 ANSI 路径可用。

调用关系:它服务于 SetScrollRegion 这个终端命令的兼容接口。真正的控制码由 SetScrollRegion::write_ansi 写出。

ResetScrollRegion::write_ansi355–357 ↗
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result

作用:这个函数生成“恢复默认滚动区域”的 ANSI 指令。它相当于告诉终端:刚才限定的滚动范围结束了。

数据流:输入是一个字符串写入目标 → 它写入重置滚动区域的控制码 → 输出格式化是否成功。

调用关系:标准插入流程在临时限制滚动区域后会调用它。没有它,后面的界面绘制可能还被困在旧的滚动范围里。

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

ResetScrollRegion::execute_winapi360–362 ↗
fn execute_winapi(&self) -> std::io::Result<()>

作用:这个函数禁止用 Windows WinAPI 方式恢复滚动区域。它要求恢复动作也必须走 ANSI 控制码。

数据流:输入是命令本身 → 如果误走这条路径就直接 panic → 不产生正常结果。

调用关系:它是 ResetScrollRegion 为 crossterm 命令接口补齐的 Windows 分支,主要作用是防止开发者误用。

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

ResetScrollRegion::is_ansi_code_supported365–368 ↗
fn is_ansi_code_supported(&self) -> bool

作用:这个函数声明重置滚动区域的命令支持 ANSI 写法。这样终端库知道可以发送控制码。

数据流:没有额外输入 → 固定返回 true → 调用方会选择 ANSI 路径。

调用关系:它配合 ResetScrollRegion::write_ansi 使用,是终端命令接口的一部分。

ModifierDiff::queue377–435 ↗
fn queue(self, mut w: W) -> io::Result<()>

作用:这个函数只输出“样式变化的差异”,比如从粗体变成普通、从无下划线变成有下划线。这样不用每个字都重置全部样式。

数据流:输入是旧样式和新样式,以及一个写入器 → 它先关闭新样式里不再需要的效果,再打开新增的效果 → 输出一串排队等待写入终端的样式控制命令。

调用关系:write_spans 在写每个文字片段前会用它切换粗体、斜体、下划线等效果。它让样式切换更精确,避免颜色和字体状态串到后面的文字。

调用图:外部调用 2 个(contains, queue!)。

write_spans438–477 ↗
fn write_spans(mut writer: &mut impl Write, content: I) -> io::Result<()>

作用:这个函数把一串带样式的文字片段写成终端能理解的内容。它会在文字之间正确切换颜色、背景色和粗体等效果。

数据流:输入是写入器和若干文字片段 → 它记住当前前景色、背景色和修饰效果,只在发生变化时发控制码,然后打印文字 → 最后重置颜色和样式,输出写入结果。

调用关系:write_history_line 会把一整行拆成片段后交给它。测试 writes_bold_then_regular_spans 专门检查它能不能从粗体正确切回普通。

调用图:被 2 处调用(writes_bold_then_regular_spans, write_history_line);外部调用 2 个(empty, queue!)。

tests::writes_bold_then_regular_spans488–513 ↗
fn writes_bold_then_regular_spans()

作用:这个测试确认写完粗体文字后,后面的普通文字不会继续被粗体影响。它防止终端样式“串味”。

数据流:输入是两个片段:粗体 A 和普通 B → 测试调用 write_spans 得到实际控制码 → 和手写的期望控制码比较,结果必须完全一致。

调用关系:它直接验证 write_spans 和 ModifierDiff::queue 的配合。若样式重置漏掉,这个测试会失败。

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

tests::vt100_blockquote_line_emits_green_fg529–561 ↗
fn vt100_blockquote_line_emits_green_fg()

作用:这个测试确认引用块这类整行带颜色的内容,写进历史后真的会显示成绿色。它防止行级样式被忽略。

数据流:输入是一行绿色引用文字和一个假的 VT100 终端 → 测试插入历史行后读取屏幕格子 → 只要看到至少一个非默认前景色单元格,就说明颜色写进去了。

调用关系:它通过 insert_history_lines 走完整插入流程,间接验证 write_history_line 会把整行样式合并到每个片段里。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_blockquote_wrap_preserves_color_on_all_wrapped_lines564–627 ↗
fn vt100_blockquote_wrap_preserves_color_on_all_wrapped_lines()

作用:这个测试确认一行绿色引用文字被换成多行后,每一行仍然保持绿色。它防止只有第一行有颜色、后续换行变白。

数据流:输入是一条很长的绿色引用和窄终端 → 插入后收集所有非空屏幕行 → 检查这些行里的非空字符都不是默认颜色。

调用关系:它覆盖 insert_history_lines 的自动换行和 write_history_line 的样式继承。长引用在终端里显示正确依赖这两部分配合。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 5 个(from, new, new, assert!, vec!)。

tests::vt100_colored_prefix_then_plain_text_resets_color630–685 ↗
fn vt100_colored_prefix_then_plain_text_resets_color()

作用:这个测试确认只有前缀有颜色时,后面的普通正文会恢复默认颜色。比如列表编号有色,但内容本身不该被染色。

数据流:输入是一行由蓝色前缀和普通正文组成的文字 → 插入到假终端后读取单元格颜色 → 检查前几个字符有颜色,后面正文是默认颜色。

调用关系:它主要验证 write_spans 的颜色切换和重置。insert_history_lines 提供完整终端写入路径。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 5 个(from, new, assert!, assert_eq!, vec!)。

tests::vt100_deep_nested_mixed_list_third_level_marker_is_colored688–736 ↗
fn vt100_deep_nested_mixed_list_third_level_marker_is_colored()

作用:这个测试检查复杂嵌套列表里,第三级的编号标记会被上色,而列表内容保持普通颜色。它保护 Markdown 渲染后的细节显示。

数据流:输入是一段多层 Markdown 列表 → 先渲染成终端行,再插入历史 → 找到包含第三级文本的屏幕行,检查编号有颜色、正文为默认颜色。

调用关系:它把 markdown_render 的输出接到 insert_history_lines,验证历史写入不会破坏 Markdown 渲染已经安排好的样式。

调用图:调用 4 个内部函数(with_options, insert_history_lines, render_markdown_text, new);外部调用 3 个(assert!, assert_eq!, new)。

tests::vt100_prefixed_url_keeps_prefix_and_url_on_same_row739–762 ↗
fn vt100_prefixed_url_keeps_prefix_and_url_on_same_row()

作用:这个测试确认带前缀的长网址不会把前缀单独留在一行。用户应该看到前缀和网址开头在同一行,阅读才自然。

数据流:输入是一行“前缀 + 长网址”和一个固定宽度终端 → 插入历史后读取屏幕行 → 检查存在同时包含前缀和网址开头的行,并且没有孤零零的前缀行。

调用关系:它验证 insert_history_lines 的 URL 换行策略。相关逻辑会避免把看起来像链接的长 token 随便硬切开。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_prefixed_url_like_without_scheme_keeps_prefix_and_token_on_same_row765–790 ↗
fn vt100_prefixed_url_like_without_scheme_keeps_prefix_and_token_on_same_row()

作用:这个测试确认没有 http 开头、但长得像网址的文本,也会被当成链接类内容照顾。这样路径或域名不会被排版弄散。

数据流:输入是一行“前缀 + 类网址文本” → 插入后读取终端屏幕 → 检查前缀和类网址开头同在一行,且没有只剩前缀的行。

调用关系:它覆盖 line_contains_url_like 相关判断在历史插入流程里的效果。insert_history_lines 是被测的完整入口。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_prefixed_mixed_url_line_wraps_suffix_words_together793–820 ↗
fn vt100_prefixed_mixed_url_line_wraps_suffix_words_together()

作用:这个测试确认一行里既有说明文字又有网址时,网址后面的普通词能像正常短语一样换行。它避免尾部文字被拆得难读。

数据流:输入是一行包含前缀、说明词、网址和尾部短语的文本 → 插入到窄终端 → 检查屏幕上既有前缀说明,也能看到尾部短语完整出现。

调用关系:它验证混合 URL 行会走自适应换行,而不是完全不处理。核心逻辑在 insert_history_hyperlink_lines_with_mode_and_wrap_policy。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_prefixed_mixed_url_line_preserves_prefix_on_wrapped_rows823–853 ↗
fn vt100_prefixed_mixed_url_line_preserves_prefix_on_wrapped_rows()

作用:这个测试确认带缩进或前缀的混合网址行,换到下一行后仍保留原来的前导空格。这样多行内容看起来还属于同一块。

数据流:输入是一行以两个空格开头、含网址和长尾文字的文本 → 插入窄终端 → 找到后续换行内容,检查它仍以两个空格开头。

调用关系:它验证 leading_whitespace_prefix 被正确用于自适应换行。插入流程通过 insert_history_lines 触发这条路径。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_prefixed_non_url_line_preserves_prefix_on_wrapped_rows856–885 ↗
fn vt100_prefixed_non_url_line_preserves_prefix_on_wrapped_rows()

作用:这个测试确认没有网址的长行在自动换行后,也会保留开头缩进。比如代码块或缩进段落不会在第二行突然贴到最左边。

数据流:输入是一条以多个空格开头的长普通文本 → 插入窄终端 → 找到换行后的内容,检查它仍然以同样的空格缩进开头。

调用关系:它同样保护 leading_whitespace_prefix 和自适应换行的配合,只是覆盖非 URL 的普通文本场景。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_terminal_wrap_policy_does_not_pre_wrap_long_paragraph888–911 ↗
fn vt100_terminal_wrap_policy_does_not_pre_wrap_long_paragraph()

作用:这个测试确认当选择“交给终端换行”策略时,Codex 不会提前按单词重新排版长段落。这样可以保留终端自己的软换行行为。

数据流:输入是一条长英文句子和 Terminal 换行策略 → 插入到窄终端 → 检查屏幕行呈现为终端按字符宽度软换行的结果。

调用关系:它直接调用 insert_history_lines_with_wrap_policy,验证 HistoryLineWrapPolicy::Terminal 这条分支没有被预先换行逻辑干扰。

调用图:调用 3 个内部函数(with_options, insert_history_lines_with_wrap_policy, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_zellij_raw_insert_keeps_soft_wrapped_tail_above_viewport914–951 ↗
fn vt100_zellij_raw_insert_keeps_soft_wrapped_tail_above_viewport()

作用:这个测试确认在 Zellij 原始插入模式下,长行软换行出来的尾巴仍停在视口上方的历史区,不会写进输入区域。

数据流:输入是一条会软换行的长文本、ZellijRaw 模式和 Terminal 换行策略 → 插入后读取屏幕快照 → 检查尾部文字在历史区域里,而不在视口区域里。

调用关系:它直接测试 insert_history_lines_with_mode_and_wrap_policy 的 ZellijRaw 分支。这个分支是为 Zellij 的特殊滚动行为准备的。

调用图:调用 3 个内部函数(with_options, insert_history_lines_with_mode_and_wrap_policy, new);外部调用 6 个(from, new, assert!, assert_snapshot!, from, vec!)。

tests::vt100_zellij_raw_replay_keeps_overflowing_soft_wrapped_tail_above_viewport954–990 ↗
fn vt100_zellij_raw_replay_keeps_overflowing_soft_wrapped_tail_above_viewport()

作用:这个测试检查更极端的 Zellij 回放场景:内容比可见历史区还高时,软换行尾部仍不能漏到视口里。

数据流:输入是一条非常长的文本和位于屏幕顶部的视口 → 用 ZellijRaw 模式插入 → 截屏并检查尾部文字只出现在视口上方的历史部分。

调用关系:它补充覆盖 insert_history_lines_with_mode_and_wrap_policy 的 ZellijRaw 溢出情况,确保大批历史重放时也安全。

调用图:调用 3 个内部函数(with_options, insert_history_lines_with_mode_and_wrap_policy, new);外部调用 7 个(from, new, assert!, format!, assert_snapshot!, from, vec!)。

tests::vt100_unwrapped_url_like_clears_continuation_rows993–1030 ↗
fn vt100_unwrapped_url_like_clears_continuation_rows()

作用:这个测试确认长网址类文本靠终端软换行时,后续行会先被清干净。否则旧内容可能残留在新网址尾部旁边。

数据流:输入先是一条很长的填充行,再是一条较短的网址类行 → 两次插入后读取屏幕 → 检查网址的续行包含正确尾部,并且没有上一条填充行的 X 残留。

调用关系:它验证 write_history_line 在宽行占多行时会清理 continuation rows(软换行续行)。这是防止终端画面脏数据的重要保护。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 4 个(from, new, assert!, vec!)。

tests::vt100_long_unwrapped_url_does_not_insert_extra_blank_gap_before_content1033–1065 ↗
fn vt100_long_unwrapped_url_does_not_insert_extra_blank_gap_before_content()

作用:这个测试确认长网址插入历史时,不会在上一条内容和网址之间多塞出明显空白。历史记录应该紧凑地接在一起。

数据流:输入先是一条提示语,再是一条非常长的网址行 → 插入后定位两条内容所在屏幕行 → 检查网址行离提示语很近,最多允许正常间隔。

调用关系:它覆盖长 URL 不预先硬换行时的行数计算和滚动插入行为。相关逻辑集中在 insert_history_hyperlink_lines_with_mode_and_wrap_policy 和 write_history_line。

调用图:调用 3 个内部函数(with_options, insert_history_lines, new);外部调用 5 个(from, new, assert!, format!, vec!)。

tui/src/thread_transcript.rs源码 ↗
domain_logicsession transcript loading / history rendering

这个文件解决的是“旧聊天记录怎么重新显示到界面上”的问题。服务器里保存的线程记录是一串原始事件,比如用户说了什么、助手回答了什么、执行过什么命令、生成过什么图片。终端界面不能直接显示这些原始事件,所以这里把它们翻译成 HistoryCell,也就是界面历史区里的显示小块。它会先读取整个线程,再逐条查看里面的内容:用户消息变成用户消息块,助手 Markdown 文本会先解析再显示,计划内容变成计划块,推理内容按设置决定显示原始推理还是摘要。对于命令、文件变更、工具调用、网页搜索这类不属于普通聊天的话,它会做一个简短的灰色说明,像收据一样告诉用户发生过什么。还有一个重要兜底:如果记录里没有任何可显示内容,它会放一行“No transcript content available”,避免界面空着让人误以为坏了。

函数细节3
load_session_transcript28–41 ↗
async fn load_session_transcript(
    app_server: &mut AppServerSession,
    thread_id: ThreadId,
    raw_reasoning_visibility: RawReasoningVisibility,
) -> std::io::Result<TranscriptCells>

作用:这个函数负责从应用服务器取回某个会话的完整历史,然后把它变成界面能显示的历史块。用户打开或切换到一个旧线程时,会需要它来恢复聊天记录。

数据流:进去的是一个服务器会话连接、一个线程编号,以及“是否显示原始推理内容”的开关。它先向服务器调用 thread_read,要求把这个线程连同每一轮对话内容一起读出来;如果读取失败,就把错误包装成普通的输入输出错误返回。读取成功后,它把拿到的 Thread 交给 thread_to_transcript_cells,最后出来的是一组 TranscriptCells,也就是可以直接塞进历史界面的显示块。

调用关系:它是这段流程的入口,由 spawn_app_server_page_loader 在加载页面时调用。它自己不负责一条条解释记录内容,而是先找 app server 拿原始线程数据,再把转换工作交给 thread_to_transcript_cells。

调用图:调用 2 个内部函数(thread_read, thread_to_transcript_cells);被 1 处调用(spawn_app_server_page_loader)。

thread_to_transcript_cells43–121 ↗
fn thread_to_transcript_cells(
    thread: &Thread,
    raw_reasoning_visibility: RawReasoningVisibility,
) -> TranscriptCells

作用:这个函数把服务器保存的线程内容,逐条翻译成终端界面里的历史显示块。它是本文件最核心的转换器,决定每种记录在用户眼里长什么样。

数据流:进去的是一个 Thread,也就是完整会话记录,以及一个控制推理内容可见性的开关。它读取线程的当前工作目录,用来正确处理 Markdown 里的文件路径;然后按顺序扫过每一轮里的每个项目。用户消息会拆出文字和图片信息做成 UserHistoryCell;助手回复会先经过 parse_assistant_markdown,把可见 Markdown 和隐藏指令分开,再做成 AgentMarkdownCell;计划会变成计划显示块;推理会根据开关选择原始内容或摘要,再变成 ReasoningSummaryCell;其他工具、命令、文件变化等事件则交给 fallback_transcript_cell 做简短说明。最后出来的是一组历史块;如果什么都没有,它会额外生成一条“没有可用记录”的提示。

调用关系:它由 load_session_transcript 在服务器数据读完后调用。它内部会调用 parse_assistant_markdown 来清理和解析助手文本,也会创建多种历史单元;遇到非聊天类事件时,它把这部分工作交给 fallback_transcript_cell,避免主流程被大量小格式塞满。

调用图:调用 5 个内部函数(parse_assistant_markdown, new, new, new, fallback_transcript_cell);被 1 处调用(load_session_transcript);外部调用 5 个(new, new, new_proposed_plan, matches!, vec!)。

fallback_transcript_cell123–233 ↗
fn fallback_transcript_cell(item: &ThreadItem) -> Option<PlainHistoryCell>

作用:这个函数给那些不是普通用户消息、助手回复、计划或推理的记录,做一个简单可读的显示说明。它像“杂项事件翻译器”,让命令执行、工具调用、文件改动等后台动作也能在历史里留下痕迹。

数据流:进去的是一个 ThreadItem,也就是线程里的一条原始事件。它根据事件类型生成一行或多行灰色文字:比如命令会显示命令本身、状态、退出码和输出;文件变化会显示变化数量;工具调用会显示工具名和状态;网页搜索会显示查询词;图片相关事件会显示图片路径或生成状态。对于已经由主流程处理过的用户消息、助手消息、计划、推理,或者不需要展示的 Sleep,它返回 None。否则它把这些文字包装成 PlainHistoryCell 返回。

调用关系:它只在 thread_to_transcript_cells 遇到“其他类型事件”时被调用。它不再调用项目里的复杂逻辑,主要用格式化字符串和列表来拼出简短说明,帮助历史界面保留完整上下文,而不是只显示聊天文本。

调用图:被 1 处调用(thread_to_transcript_cells);外部调用 2 个(format!, vec!)。