Codex 系统手册

App 级事件分发与线程路由

stage-10.1.110 个文件

这一阶段是终端界面的“交通枢纽”,属于主干活儿的循环。用户按键、后台消息、审批请求都会先变成统一消息或命令,再由总分诊台分发。输入模块管全局快捷键;后台事件和请求模块把服务器来的通知、待审批事项放到正确位置;线程路由负责多条聊天线切换和回放;未完成交互只重放还在等人的提示。慢任务丢到后台跑,重画请求也被合并,保证界面不乱、不假死。

本阶段的文件10

事件进入与分发

这些文件定义应用事件如何进入 TUI,并被路由到具体的应用行为和重绘调度。

tui/src/app_event_sender.rs源码 ↗
io_transportcross-cutting

这个文件的核心是 AppEventSender,可以把它想成前台界面和后台应用之间的传话筒。界面里很多地方都会产生动作:用户按了 Ctrl+C、同意执行命令、拒绝修改文件、回答工具提问、要求重新列出技能等等。没有这个封装,每个地方都要自己组装 AppEvent 和 AppCommand,容易写错,也容易漏掉日志。这里统一做两件事:第一,把各种常见动作包装成应用能看懂的事件;第二,发送前把多数事件记到 session log(会话日志,用来以后高保真回放当时发生了什么)。真正发送靠 tokio 的 mpsc 无界通道(任务之间传消息的队列),如果接收端已经没了,它不会让程序崩掉,只会写一条错误日志。这个文件不真正执行命令,它只是把“要做什么”安全、统一地投递给后面的主循环。

函数细节13
AppEventSender::new28–30 ↗
fn new(app_event_tx: UnboundedSender<AppEvent>) -> Self

作用:创建一个 AppEventSender,把底层的消息通道包起来。之后其他代码就可以用更好懂的方法名来发事件,而不是直接操作原始通道。

数据流:进去的是一个可以发送 AppEvent 的通道发送端 → 它把这个发送端保存到结构体里 → 出来的是一个 AppEventSender,后续所有事件都会通过它送进应用。

调用关系:程序启动的 run 会创建它,很多测试和界面提示流程也会创建它。它本身不发送消息,只是把后面 AppEventSender::send 和各种快捷方法需要用的通道准备好。

调用图:被 345 处调用(run, render_skill_load_warning_cells, accepted_model_migration_persists_target_default_reasoning_effort, auth_suggestion_with_reason_snapshot, declined_tool_suggestion_resolves_elicitation_decline, enable_suggestion_with_reason_snapshot, enable_tool_suggestion_resolves_elicitation_after_enable, generic_url_elicitation_confirmation_snapshot, generic_url_elicitation_resolves_without_connector_refresh, generic_url_elicitation_snapshot (+15 more))。

AppEventSender::send34–43 ↗
fn send(&self, event: AppEvent)

作用:把一个已经组装好的 AppEvent 发进应用事件队列。它还会顺手记录会话日志,方便之后复盘或回放用户当时做了什么。

数据流:进去的是一个 AppEvent → 如果它不是 CodexOp 这类已经在别处记录过的操作,就先交给 log_inbound_app_event 写入会话日志 → 然后通过内部通道发送出去;如果发送失败,就写错误日志,不把错误继续抛给调用者。

调用关系:这是本文件所有快捷发送方法的共同出口。handle_tui_event、启动线程、配置警告、技能加载警告等很多地方也会直接用它,把事件送到应用主循环。

调用图:调用 1 个内部函数(log_inbound_app_event);被 53 处调用(handle_tui_event, send_world_writable_scan_failed, spawn_startup_thread_start, apply_accepted_model_migration, emit_project_config_warnings, emit_skill_load_warnings, emit_system_bwrap_warning, handle_model_migration_prompt_if_needed, compact, exec_approval (+15 more));外部调用 3 个(send, matches!, error!)。

AppEventSender::interrupt45–47 ↗
fn interrupt(&self)

作用:发送“中断当前工作”的命令,常见场景是用户按 Ctrl+C 想让正在进行的任务停下来。

数据流:它不需要额外输入 → 先用 AppCommand::interrupt 生成一个中断命令,再包成 AppEvent::CodexOp → 最后交给 AppEventSender::send 发出去。

调用关系:handle_key_event 和 on_ctrl_c 会在用户触发中断时调用它。它只是发出请求,真正停下任务的是后面的应用处理流程。

调用图:调用 1 个内部函数(send);被 2 处调用(handle_key_event, on_ctrl_c);外部调用 2 个(interrupt, CodexOp)。

AppEventSender::interrupt_and_restore_prompt_if_no_output49–53 ↗
fn interrupt_and_restore_prompt_if_no_output(&self)

作用:发送一种更细的中断命令:如果被中断的任务还没有输出内容,就把输入提示恢复回来。这样用户不会看到一个卡住或消失的提示框。

数据流:它不接收输入 → 创建 interrupt_and_restore_prompt_if_no_output 命令,包成 CodexOp 事件 → 通过 AppEventSender::send 放进事件队列。

调用关系:更高层的 interrupt 流程会用到它,用来处理“中断但界面还要恢复得自然”的情况。它把具体执行交给 AppCommand 和后续事件处理器。

调用图:调用 1 个内部函数(send);被 1 处调用(interrupt);外部调用 2 个(interrupt_and_restore_prompt_if_no_output, CodexOp)。

AppEventSender::compact55–57 ↗
fn compact(&self)

作用:发送“压缩上下文”的命令。简单说,就是请求系统把当前对话或工作内容整理得更省空间,方便继续往下用。

数据流:没有外部输入 → 生成 AppCommand::compact 命令并包装成 CodexOp → 通过 send 发送,等待主流程处理。

调用关系:它是一个快捷按钮式的方法。调用者不用知道 compact 命令具体怎么组装,只要调用这个方法,就能把请求交给应用主流程。

调用图:调用 1 个内部函数(send);外部调用 2 个(compact, CodexOp)。

AppEventSender::set_thread_name59–61 ↗
fn set_thread_name(&self, name: String)

作用:发送“给某个会话线程改名”的命令。这里的线程可以理解成一条独立的对话或工作线索。

数据流:进去的是新的名称字符串 → 它把名称塞进 AppCommand::set_thread_name → 包成 CodexOp 并通过 send 发出去;真正改名发生在后续处理阶段。

调用关系:界面或其他流程想改显示名称时会用它。它负责把用户给出的名字变成应用内部能处理的命令。

调用图:调用 1 个内部函数(send);外部调用 2 个(set_thread_name, CodexOp)。

AppEventSender::review63–65 ↗
fn review(&self, target: ReviewTarget)

作用:发送“开始审查某个目标”的命令。目标可能是一次改动、一段代码或应用协议里定义的其他 ReviewTarget。

数据流:进去的是 ReviewTarget,也就是要审查的对象 → 它调用 AppCommand::review 生成命令 → 包成 CodexOp 后交给 send。

调用关系:它位于界面请求和后台审查流程之间。调用者只说明要审查什么,后面的主流程再决定如何执行审查。

调用图:调用 1 个内部函数(send);外部调用 2 个(review, CodexOp)。

AppEventSender::list_skills67–72 ↗
fn list_skills(&self, cwds: Vec<PathBuf>, force_reload: bool)

作用:发送“列出技能”的命令。技能可以理解成系统可用的一组能力或工具说明,这个方法还能要求强制重新加载。

数据流:进去的是一组目录路径 cwds 和一个 force_reload 开关 → 它们被放进 AppCommand::list_skills → 包成 CodexOp 后发送;结果不会在这里直接返回,而是由后续事件再更新界面。

调用关系:close 流程会调用它,比如关闭某些界面时触发技能列表刷新。它把“去哪儿找技能、要不要重新读”的信息传给后台。

调用图:调用 1 个内部函数(send);被 1 处调用(close);外部调用 2 个(list_skills, CodexOp)。

AppEventSender::user_input_answer74–78 ↗
fn user_input_answer(&self, id: String, response: ToolRequestUserInputResponse)

作用:把用户对工具提问的回答发回去。比如某个工具需要用户补充信息,这个方法负责把答案送回应用。

数据流:进去的是提问编号 id 和用户回答 response → 它们被组装成 AppCommand::user_input_answer → 包成 CodexOp 后通过 send 发出。

调用关系:submit_answers 和 submit_empty_auto_resolution 会在提交答案或自动提交空答案时调用它。它连接了界面里的输入表单和后台等待答案的工具请求。

调用图:调用 1 个内部函数(send);被 2 处调用(submit_answers, submit_empty_auto_resolution);外部调用 2 个(user_input_answer, CodexOp)。

AppEventSender::exec_approval80–90 ↗
fn exec_approval(
        &self,
        thread_id: ThreadId,
        id: String,
        decision: CommandExecutionApprovalDecision,
    )

作用:发送用户对“是否允许执行某条命令”的决定。比如后台想运行 shell 命令时,界面让用户批准或拒绝,这个方法就把决定传回去。

数据流:进去的是 thread_id、请求 id 和批准/拒绝的 decision → 它用 AppCommand::exec_approval 生成操作 → 再包成 AppEvent::SubmitThreadOp,表示这条操作要送到指定线程 → 最后通过 send 发出。

调用关系:handle_exec_decision 在用户点选执行决定后调用它。它和普通 CodexOp 不同,会带上 thread_id,确保决定回到正确的会话线程。

调用图:调用 1 个内部函数(send);被 1 处调用(handle_exec_decision);外部调用 1 个(exec_approval)。

AppEventSender::request_permissions_response92–102 ↗
fn request_permissions_response(
        &self,
        thread_id: ThreadId,
        id: String,
        response: RequestPermissionsResponse,
    )

作用:发送用户对“权限请求”的回复。权限请求就是系统想获得某些操作许可时,向用户确认的过程。

数据流:进去的是 thread_id、请求 id 和权限回复 response → 它组装 AppCommand::request_permissions_response → 放进 SubmitThreadOp,指定要交给哪个线程 → 通过 send 发到事件队列。

调用关系:handle_permissions_decision 会在用户处理权限弹窗或提示后调用它。它保证权限答复不会发错地方,而是回到提出请求的线程。

调用图:调用 1 个内部函数(send);被 1 处调用(handle_permissions_decision);外部调用 1 个(request_permissions_response)。

AppEventSender::patch_approval104–114 ↗
fn patch_approval(
        &self,
        thread_id: ThreadId,
        id: String,
        decision: FileChangeApprovalDecision,
    )

作用:发送用户对“是否允许修改文件”的决定。patch 指的是文件改动补丁,这里处理的是同意或拒绝这类改动。

数据流:进去的是 thread_id、改动请求 id 和 decision → 它调用 AppCommand::patch_approval 生成命令 → 包成指定线程的 SubmitThreadOp → 用 send 送出。

调用关系:handle_patch_decision 会在用户批准或拒绝文件改动时调用它。它把界面上的选择变成后台能继续处理的文件变更审批结果。

调用图:调用 1 个内部函数(send);被 1 处调用(handle_patch_decision);外部调用 1 个(patch_approval)。

AppEventSender::resolve_elicitation116–129 ↗
fn resolve_elicitation(
        &self,
        thread_id: ThreadId,
        server_name: String,
        request_id: AppServerRequestId,
        decision: McpServerElicitationAction,
        content:

作用:回复一个来自 MCP 服务器的“征询”。征询可以理解成外部工具或服务器向用户追问:你想怎么处理、是否同意、要补充什么内容。

数据流:进去的是线程 id、服务器名、请求 id、用户决定 decision,以及可选的 content 和 meta 这两包 JSON 数据 → 它们被组装成 AppCommand::resolve_elicitation → 放进 SubmitThreadOp 指向正确线程 → 最后通过 send 发出。

调用关系:handle_elicitation_decision、dispatch_cancel、submit_answers 等流程会调用它,分别对应用户做决定、取消请求或提交答案。它负责把这些回复准确送回发起征询的线程和服务器请求。

调用图:调用 1 个内部函数(send);被 4 处调用(resolve_elicitation, handle_elicitation_decision, dispatch_cancel, submit_answers);外部调用 1 个(resolve_elicitation)。

tui/src/tui/frame_requester.rs源码 ↗
domain_logiccross-cutting;TUI 启动后创建,运行期间任何需要刷新界面的地方都会用到

在文字界面里,很多事情都会触发刷新:用户按键、动画下一帧、窗口大小变化、后台任务状态更新。这个文件提供一个小把手 FrameRequester,其他代码拿到它以后,就可以请求“马上刷新”或“过一会儿刷新”。真正排队和合并请求的是后台任务 FrameScheduler。它像一个前台叫号员:大家都来拿号,但如果好几个人几乎同时叫刷新,它只通知主界面画一次。它还通过 FrameRateLimiter 限制最快刷新速度,最多约 120 FPS(每秒 120 帧),防止刷新太密。请求用 mpsc 通道传给调度器,最后用 broadcast 通道通知主 TUI 事件循环“可以画了”。文件后半部分是测试,检查立即刷新、延迟刷新、多个请求合并、以及限速是否都按预期工作。

函数细节14
FrameRequester::new39–46 ↗
fn new(draw_tx: broadcast::Sender<()>) -> Self

作用:创建一个可以到处传递的刷新请求器,并同时启动背后的刷新调度任务。别人以后只要拿着这个对象,就能要求界面重画。

数据流:进去的是一个 broadcast 发送端,也就是给主界面事件循环发“该画了”通知的喇叭。函数先建一条内部 mpsc 队列(一种多发送者、单接收者的消息通道),再创建 FrameScheduler,把它用 tokio::spawn 放到后台跑。出来的是 FrameRequester,里面保存着向调度器递交刷新时间的发送端。

调用关系:这是整套机制的入口。主 TUI 或测试会先调用它;它把真正的排队工作交给 FrameScheduler::new 和后台的 FrameScheduler::run。后续 schedule_frameschedule_frame_in 发出的请求,都会沿着这里创建的通道进入那个后台调度器。

调用图:调用 1 个内部函数(new);被 9 处调用(new, test_coalesces_mixed_immediate_and_delayed_requests, test_coalesces_multiple_requests_into_single_draw, test_limits_draw_notifications_to_120fps, test_multiple_delayed_requests_coalesce_to_earliest, test_rate_limit_clamps_early_delayed_requests, test_rate_limit_does_not_delay_future_draws, test_schedule_frame_immediate_triggers_once, test_schedule_frame_in_triggers_at_delay);外部调用 2 个(unbounded_channel, spawn)。

FrameRequester::schedule_frame49–51 ↗
fn schedule_frame(&self)

作用:请求界面尽快重画一次。它适合用户操作、状态变化、动画推进这类“现在画一下”的场景。

数据流:进去的是当前这个请求器本身。函数读取当前时间 Instant::now(),把“现在”这个时间点发进内部队列。它不直接画屏幕,也不等待结果;发送成功后,后台调度器稍后会决定什么时候通知主界面画。

调用关系:很多 TUI 组件都会调用它,比如按键处理、随机选择显示变体、请求重画、设置高亮等。它只是把请求递给 FrameScheduler::run,由调度器负责合并多个请求并遵守刷新限速。

调用图:被 33 处调用(handle_draw_size_change, pick_random_variant, schedule_next_frame, request_redraw, request_redraw, handle_key, select, set_highlight, back_to_summary, customize (+15 more));外部调用 2 个(now, send)。

FrameRequester::schedule_frame_in54–56 ↗
fn schedule_frame_in(&self, dur: Duration)

作用:请求界面在指定时间之后重画一次。它适合动画、延迟提示、粘贴节流等需要“过一小会儿再刷新”的场景。

数据流:进去的是一个时长 Duration,表示还要等多久。函数拿当前时间加上这段时长,得到目标刷新时间,然后把这个时间点发给内部队列。出来没有直接返回值;实际效果是后台调度器收到一个未来的刷新请求。

调用关系:窗口尺寸变化、动画下一帧、延迟重画、浏览器继续渲染等流程会用它。它和 schedule_frame 走同一条通道,最后都交给 FrameScheduler::run 做合并和限速。

调用图:被 8 处调用(handle_draw_size_change, schedule_next_frame, request_redraw_in, handle_paste_burst_tick, render, render_continue_in_browser, schedule_next_frame, render);外部调用 2 个(now, send)。

FrameRequester::test_dummy62–67 ↗
fn test_dummy() -> Self

作用:给测试用的假刷新请求器。它可以接收刷新请求,但不会真的启动调度器,也不会通知界面重画。

数据流:函数创建一个内部通道,只保留发送端,把接收端丢掉。出来的是一个 FrameRequester,测试代码可以把它塞给需要刷新请求器的组件,但发送出去的消息没人处理。

调用关系:很多测试会调用它,用来绕开真实的异步刷新机制。这样测试重点可以放在聊天组件、输入框、弹窗、历史搜索等行为上,而不必真的跑一套界面刷新调度。

调用图:被 135 处调用(enqueue_primary_thread_session_replays_turns_before_initial_prompt_submit, height_shrink_schedules_resize_reflow, replace_chat_widget_reseeds_collab_agent_metadata_for_replay, composer_shown_after_denied_while_task_running, ctrl_c_cancels_history_search_without_clearing_draft_or_showing_quit_hint, ctrl_c_on_modal_consumes_without_showing_quit_hint, drain_pending_submission_state_clears_remote_image_urls, esc_interrupts_running_task_when_no_popup, esc_release_after_dismissing_agent_picker_does_not_interrupt_task, esc_routes_to_handle_key_event_when_requested (+15 more));外部调用 1 个(unbounded_channel)。

FrameScheduler::new84–90 ↗
fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self

作用:创建真正干活的刷新调度器。它拿到请求队列和通知喇叭,并准备好刷新限速器。

数据流:进去的是内部请求接收端,以及给主 TUI 事件循环发通知的 broadcast 发送端。函数把它们保存起来,同时创建默认的 FrameRateLimiter。出来的是一个 FrameScheduler,还没有开始跑,要由外面启动它的 run

调用关系FrameRequester::new 会调用它。它是请求器和后台调度循环之间的装配步骤:先把零件组好,再由 tokio::spawnFrameScheduler::run 在后台持续工作。

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

FrameScheduler::run96–127 ↗
async fn run(mut self)

作用:后台循环,负责把很多刷新请求合并成尽量少的真正刷新通知。它还保证刷新不会快到超过限制。

数据流:进去的是调度器自己,里面有请求接收队列、画面通知发送端和限速器。它反复等待两件事:要么收到一个目标刷新时间,要么等到当前最早的刷新截止时间。收到请求后,它会用限速器把太早的时间往后推,并只保留最早该画的时间;等时间到了,它发送一个空通知 () 给主界面,表示“可以重画了”,然后清空当前截止时间。如果所有请求发送端都没了,它就退出循环。

调用关系:它由 FrameRequester::new 创建并放到 Tokio 异步运行时后台执行。schedule_frameschedule_frame_in 发来的所有请求都会到这里汇合;它最后通过 draw_tx.send(()) 把结果通知给主 TUI 事件循环。

调用图:外部调用 4 个(from_secs, pin!, select!, sleep_until)。

tests::test_schedule_frame_immediate_triggers_once137–157 ↗
async fn test_schedule_frame_immediate_triggers_once()

作用:测试“马上刷新”真的会发出一次通知,而且不会多发。它防止一个简单刷新请求意外造成重复绘制。

数据流:测试先创建通知通道和 FrameRequester,调用 schedule_frame,再推进虚拟时间。然后它从接收端等第一条通知,确认收到了;接着再短暂等待,确认没有第二条多余通知。

调用关系:它调用 FrameRequester::new 搭起真实调度器,再通过 schedule_frame 进入 FrameScheduler::run。这个测试覆盖的是最基础的立即刷新路径。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_schedule_frame_in_triggers_at_delay160–183 ↗
async fn test_schedule_frame_in_triggers_at_delay()

作用:测试“延迟刷新”不会提前发生,并且到时间后只发生一次。

数据流:测试创建请求器后,要求 50 毫秒后刷新。它先推进 30 毫秒并确认收不到通知,再继续推进超过目标时间,确认能收到一次通知,最后确认没有额外通知。

调用关系:它通过 FrameRequester::new 启动调度器,并调用 schedule_frame_in 发送未来时间。重点验证 FrameScheduler::run 的定时等待逻辑。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_coalesces_multiple_requests_into_single_draw186–209 ↗
async fn test_coalesces_multiple_requests_into_single_draw()

作用:测试多个几乎同时的立即刷新请求会被合并成一次画面刷新。这样能避免一个操作连环触发三四次重画。

数据流:测试连续调用三次 schedule_frame,再推进一点虚拟时间。它确认只收到一条刷新通知,并确认后面没有第二条、第三条通知。

调用关系:它搭起真实的 FrameRequesterFrameScheduler,用连续请求模拟组件扎堆喊“刷新”。它验证的是调度器“合并请求”的核心价值。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_coalesces_mixed_immediate_and_delayed_requests212–232 ↗
async fn test_coalesces_mixed_immediate_and_delayed_requests()

作用:测试一个延迟刷新和一个立即刷新碰在一起时,会合并到更早的那次刷新里。也就是说,立即刷新发生后,不会再为了之前那个延迟请求额外画一次。

数据流:测试先请求 100 毫秒后刷新,再请求立即刷新。推进一点时间后,它确认收到一次通知;随后继续等待足够久,确认不会因为那个 100 毫秒请求再收到第二次通知。

调用关系:它调用 schedule_frame_inschedule_frame,让两种请求进入同一个 FrameScheduler::run。这个测试保证调度器会选最早的刷新时间,并把后面的请求一起吃掉。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_limits_draw_notifications_to_120fps235–263 ↗
async fn test_limits_draw_notifications_to_120fps()

作用:测试刷新通知不会超过 120 FPS 的速度上限。它防止界面因为刷新太频繁而占满资源。

数据流:测试先请求并收到第一次刷新。随后立刻再请求一次刷新,并推进很短时间,确认第二次不会太早到来。等推进到最小帧间隔之后,再确认第二次通知可以收到。

调用关系:它通过 FrameRequester::new 走完整调度路径,重点验证 FrameScheduler::run 会使用 FrameRateLimiter 把过早的刷新往后推。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_rate_limit_clamps_early_delayed_requests266–295 ↗
async fn test_rate_limit_clamps_early_delayed_requests()

作用:测试即使请求的是“稍后刷新”,如果这个“稍后”仍然太靠近上一帧,也会被限速器推迟。

数据流:测试先触发并收到一帧。然后请求 1 毫秒后再画。它推进不到最小间隔的一半,确认没有通知;再推进足够时间,确认第二帧才出现。

调用关系:它结合 schedule_frameschedule_frame_in,验证 FrameScheduler::run 对延迟请求也会调用限速器,而不是只限制立即请求。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_rate_limit_does_not_delay_future_draws298–324 ↗
async fn test_rate_limit_does_not_delay_future_draws()

作用:测试如果请求的刷新时间本来就在足够远的未来,限速器不会无故再拖延。它防止动画或定时刷新变慢。

数据流:测试先完成一次刷新,再请求 50 毫秒后刷新。它推进到 49 毫秒时确认还没画,再推进 1 毫秒,确认准时收到通知。

调用关系:它通过 schedule_frame_in 检查 FrameScheduler::runFrameRateLimiter 的配合:只拦住太早的请求,不影响本来合理的未来请求。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tests::test_multiple_delayed_requests_coalesce_to_earliest327–353 ↗
async fn test_multiple_delayed_requests_coalesce_to_earliest()

作用:测试多个延迟刷新请求会合并到最早的那个时间点,只画一次。这样后面的类似请求不会造成重复刷新。

数据流:测试分别请求 100 毫秒、20 毫秒、120 毫秒后的刷新。它先推进到早于最早时间,确认没有通知;再推进超过最早时间,确认收到一次通知;最后继续等待,确认不会再为 100 毫秒和 120 毫秒各画一次。

调用关系:它用多个 schedule_frame_in 请求喂给同一个 FrameScheduler::run。这个测试验证调度器会记住“最早该画的时间”,并把同一批延迟请求合并掉。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_millis, assert!, channel, advance)。

tui/src/app/event_dispatch.rs源码 ↗
orchestrationmain loop

这个文件主要给 App 增加一套事件分发能力。可以把它想成医院前台:用户点了按钮、后台传来结果、配置写完、服务器返回数据,都会变成 AppEvent,然后由 App::handle_event 看清楚是哪一种事,再决定是更新聊天界面、调用 app server、打开弹窗、写配置,还是退出程序。它本身不尽量做“大活”,而是把插件、权限、Windows 沙箱、快捷键、历史记录、速率限制等具体工作交给别的模块。文件里还单独处理了几件容易出问题的事:退出时先尝试关闭当前线程但最多等 2 秒;归档和删除前确认真的有当前会话;改快捷键时先验证、再保存、再刷新运行中的快捷键。它的重要性在于让整个 TUI 的行为有一个统一入口,避免同一个事件在不同地方被重复或冲突地处理。

函数细节7
App::handle_event16–2111 ↗
async fn handle_event(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        event: AppEvent,
    ) -> Result<AppRunControl>

作用:这是整个 TUI 应用处理事件的总入口。只要应用里发生一件“需要反应的事”,比如新建会话、提交消息、打开插件页、保存设置、退出程序,基本都会先经过这里。

数据流:进去的是当前 App 状态、TUI 界面对象、app server 会话,以及一个 AppEvent。它先判断事件类型,再读取或改动聊天窗口、配置、当前线程、弹窗、后台请求状态等信息;需要后台帮忙时就调用 app_server 或其他模块;需要重画界面时就请求刷新。出来的是 AppRunControl,告诉外层循环继续运行,还是带着某个原因退出。

调用关系:它是本文件的核心调度器。很多分支会把具体工作交给别的函数,例如改快捷键时交给 App::apply_keymap_capture 或 App::apply_keymap_clear;插件配置写完后会调用 App::refresh_plugin_mentions_after_config_write;退出、归档、删除则分别交给 App::handle_exit_mode、App::archive_current_thread、App::delete_current_thread。它自己像交通警察,决定每辆车该往哪条路走。

调用图:调用 32 个内部函数(from, status_line_items_edit, status_line_use_colors_edit, syntax_theme_edit, terminal_title_items_edit, active, from_config, apply_keymap_capture, apply_keymap_clear, archive_current_thread (+15 more));外部调用 37 个(new, now, from, new, personality_label, reasoning_label_for, spawn_world_writable_scan, new, override_turn_context, new (+15 more))。

App::apply_keymap_capture2113–2179 ↗
async fn apply_keymap_capture(
        &mut self,
        context: String,
        action: String,
        key: String,
        intent: crate::app_event::KeymapEditIntent,
    )

作用:这个函数用来保存用户刚录入的新快捷键。它不只是把按键写进去,还会检查这个快捷键能不能用、会不会冲突,并把运行中的快捷键表一起更新。

数据流:进去的是快捷键所属的场景 context、动作 action、用户按下的 key,以及编辑意图 intent。它先根据旧配置算出新配置;如果发现输入不合法,就在聊天界面显示错误;如果发现冲突,就打开一个让用户选择的界面;如果没问题,就把新绑定写入配置文件,并更新 App 里的配置、运行时快捷键和聊天界面的显示。出来没有单独返回值,但会改变快捷键状态,或显示成功、错误、冲突提示。

调用关系:它由 App::handle_event 在收到 KeymapCaptured 事件时调用。它会借助 keymap_setup 相关函数生成新快捷键配置,用 RuntimeKeymap::from_config 把配置变成运行时可用的快捷键表,再用 ConfigEditsBuilder 保存到配置里。完成后,它把结果反馈给 chat_widget,让用户马上看到新快捷键已经生效。

调用图:调用 4 个内部函数(keymap_bindings_edit, from_config, build_keymap_conflict_params, keymap_with_edit);被 1 处调用(handle_event);外部调用 3 个(for_config, format!, error!)。

App::refresh_plugin_mentions_after_config_write2181–2184 ↗
fn refresh_plugin_mentions_after_config_write(&mut self)

作用:这个小函数在插件相关配置被改动后使用,用来刷新“@ 插件提及”这类提示内容。它保证界面和后台读到的用户配置同步,不会还显示旧插件。

数据流:进去的是当前 App。它让聊天界面重新加载插件提及列表,同时提交一个 reload_user_config 命令给后端,让正在工作的会话重新读取用户配置。出来没有返回值,但会触发界面刷新和后台配置重载。

调用关系:它由 App::handle_event 在插件安装、卸载、启用状态变化、市场源变化等成功写配置之后调用。它连接了“配置已经变了”和“界面及后台也要知道”这两步,避免插件列表看起来更新了,但真正提交给模型或后端的配置还是旧的。

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

App::apply_keymap_clear2186–2232 ↗
async fn apply_keymap_clear(&mut self, context: String, action: String)

作用:这个函数用来删除某个动作的自定义快捷键,让它回到默认或无自定义绑定的状态。用户在快捷键设置里选择清除绑定时会用到它。

数据流:进去的是要清除的场景 context 和动作 action。它先从当前配置里移除这条自定义绑定,再尝试生成新的运行时快捷键表;如果失败,就显示错误。成功后,它把“清除绑定”的修改写入配置文件,更新 App 内存里的配置和运行时快捷键,并让聊天界面回到快捷键选择页显示最新状态。出来没有返回值,但会改变配置、快捷键表和界面提示。

调用关系:它由 App::handle_event 在收到 KeymapCleared 事件时调用。它和 App::apply_keymap_capture 是一对:一个负责新增或替换快捷键,一个负责清除快捷键。两者都会通过 RuntimeKeymap::from_config 检查新配置能不能实际运行,并通过 ConfigEditsBuilder 持久保存。

调用图:调用 3 个内部函数(keymap_binding_clear_edit, from_config, keymap_without_custom_binding);被 1 处调用(handle_event);外部调用 3 个(for_config, format!, error!)。

App::handle_exit_mode2234–2269 ↗
async fn handle_exit_mode(
        &mut self,
        app_server: &mut AppServerSession,
        mode: ExitMode,
    ) -> AppRunControl

作用:这个函数决定程序要怎么退出。它支持两种方式:一种是先尽量通知当前会话关闭,再退出;另一种是立刻退出。

数据流:进去的是当前 App、app_server 和退出模式 mode。如果模式是 ShutdownFirst,它会记下正在关闭的线程,尝试调用 shutdown_current_thread 关闭当前会话,但最多等 2 秒,避免用户按了退出却卡住;之后清掉这个标记并返回退出指令。如果模式是 Immediate,它直接清掉标记并返回退出指令。出来的是 AppRunControl::Exit,表示外层运行循环应该结束。

调用关系:它由 App::handle_event 在处理 Exit 事件时调用,Logout 成功后也会走这条路。它会把真正的会话关闭工作交给 shutdown_current_thread,并用 timeout 给这件事设一个“别让用户久等”的上限。这样既尽量礼貌地收尾,又不让坏掉的后台服务拖住整个界面。

调用图:被 1 处调用(handle_event);外部调用 3 个(timeout, warn!, Exit)。

App::archive_current_thread2271–2296 ↗
async fn archive_current_thread(
        &mut self,
        app_server: &mut AppServerSession,
    ) -> AppRunControl

作用:这个函数用来归档当前聊天线程。归档成功后,当前 TUI 会退出,因为这个会话已经被用户明确收起来了。

数据流:进去的是当前 App 和 app_server。它先找当前线程 ID;如果还没有线程,就在界面显示“需要先开始线程”;如果当前是 side conversation(旁支对话),也会拒绝归档并提示用户先回到主线程。检查通过后,它调用 app_server.thread_archive。成功则返回退出指令;失败则把错误显示到聊天界面,并返回继续运行。

调用关系:它由 App::handle_event 在收到 ArchiveCurrentThread 事件时调用。真正改服务器状态的动作交给 app_server.thread_archive,本函数负责做用户层面的保护和结果翻译:能归档就退出,不能归档就把原因说清楚并留在界面里。

调用图:调用 1 个内部函数(thread_archive);被 1 处调用(handle_event);外部调用 2 个(format!, Exit)。

App::delete_current_thread2298–2323 ↗
async fn delete_current_thread(
        &mut self,
        app_server: &mut AppServerSession,
    ) -> AppRunControl

作用:这个函数用来删除当前聊天线程。删除是更危险的动作,所以它会先确认确实有当前线程,并且不允许在旁支对话里直接删除。

数据流:进去的是当前 App 和 app_server。它先取得当前线程 ID;没有线程时会显示错误;如果线程属于 side_threads,也会显示“旁支对话里不能删除”的提示。通过检查后,它调用 app_server.thread_delete 删除线程。删除成功会返回退出指令;删除失败会把失败原因显示在聊天界面,并让应用继续运行。

调用关系:它由 App::handle_event 在收到 DeleteCurrentThread 事件时调用。它和 App::archive_current_thread 结构很像,但调用的是 app_server.thread_delete。它承担的是删除前的安全检查和用户反馈,真正的删除动作由 app server 完成。

调用图:调用 1 个内部函数(thread_delete);被 1 处调用(handle_event);外部调用 2 个(format!, Exit)。

全局输入与命令词汇

这些文件描述应用范围的命令语言,以及将用户输入转换为这些操作的顶层键盘处理。

tui/src/app_command.rs源码 ↗
data_modelcross-cutting

这个文件的核心是 AppCommand。可以把它理解成终端界面和应用后台之间传递的“工单格式”:用户输入了一句话、想中断任务、批准执行命令、要求回滚对话、请求代码审查,这些都被做成不同种类的 AppCommand。这样做的好处是,别的代码不用到处传一堆零散参数,只要传一个命令对象,接收方看命令种类就知道该干什么。文件里还定义了 InterruptBehavior,也就是“中断时怎么收尾”:普通中断,或者如果还没输出内容就把输入提示恢复回来。后半部分是一组创建命令的小函数,比如 user_turn、exec_approval、review,它们像填表按钮一样,把需要的信息放进正确的命令格子里。这个文件本身不真正执行命令,它只是把“要做什么”和“相关材料”打包清楚。

函数细节21
AppCommand::interrupt114–118 ↗
fn interrupt() -> Self

作用:创建一个普通的“中断当前操作”命令。用户想停止正在进行的回答或任务时,会需要这种命令。

数据流:进去不需要任何参数 → 它生成一个 AppCommand::Interrupt,并把中断方式设为默认 → 出来的是一张“请中断”的命令纸条,不直接停止任何东西。

调用关系:它通常由界面层在用户触发中断时调用,然后把生成的命令交给应用主流程;真正怎么中断,是后面的命令处理者决定。

AppCommand::interrupt_and_restore_prompt_if_no_output120–124 ↗
fn interrupt_and_restore_prompt_if_no_output() -> Self

作用:创建一个更细致的中断命令:如果任务还没输出内容,就恢复输入提示。它适合那种用户取消操作后,希望界面看起来像没开始过一样的场景。

数据流:进去不需要任何参数 → 它生成 AppCommand::Interrupt,并把行为设为 RestorePromptIfNoOutput → 出来的是一个带特殊收尾要求的中断命令。

调用关系:它和 AppCommand::interrupt 一样交给后续流程处理,只是多带了一个提示:如果没有输出,就把提示栏恢复好。

AppCommand::clean_background_terminals126–128 ↗
fn clean_background_terminals() -> Self

作用:创建一个“清理后台终端”的命令。它用于让系统收拾那些后台运行过、现在需要整理或关闭的终端资源。

数据流:进去不需要参数 → 它选择 CleanBackgroundTerminals 这个命令种类 → 出来的是一条清理后台终端的请求。

调用关系:界面或调度代码需要做资源清理时会创建它,之后由应用内部负责真正清理后台终端。

AppCommand::run_user_shell_command130–132 ↗
fn run_user_shell_command(command: String) -> Self

作用:创建一个“运行用户输入的 shell 命令”的请求。shell 命令就是用户平时在终端里敲的命令,比如列文件、运行脚本。

数据流:进去一个 command 字符串 → 它把这个字符串放进 RunUserShellCommand → 出来的是一条带着具体命令文本的执行请求。

调用关系:当用户明确要求运行一条终端命令时,调用方会用它打包;真正执行命令的部分在后面的命令处理流程里。

AppCommand::user_turn135–162 ↗
fn user_turn(
        items: Vec<UserInput>,
        cwd: PathBuf,
        approval_policy: AskForApproval,
        active_permission_profile: Option<ActivePermissionProfile>,
        model: String,

作用:创建一个新的“用户回合”命令,也就是用户这次发给助手的输入和上下文。它是开始一次对话处理的主要入口之一。

数据流:进去用户输入列表、当前目录、审批策略、权限配置、模型名、推理强度、摘要设置、服务档位、输出格式要求、协作模式和人格设置 → 它把这些信息装进 UserTurn,并把 approvals_reviewer 固定为 None → 出来的是一条完整的“请处理这轮用户输入”的命令。

调用关系:界面收集到用户消息后会调用它;后续应用主循环拿到这个命令,再根据里面的目录、模型、权限和输入内容启动一次助手响应。

AppCommand::override_turn_context165–193 ↗
fn override_turn_context(
        cwd: Option<PathBuf>,
        approval_policy: Option<AskForApproval>,
        approvals_reviewer: Option<ApprovalsReviewer>,
        permission_profile: Option<Permi

作用:创建一个“临时改上下文”的命令。上下文就是这一轮运行时的环境,比如工作目录、模型、权限策略等。

数据流:进去一批可选值:目录、审批策略、审核者、权限配置、沙盒级别、模型、推理设置、摘要、服务档位、协作模式、人格 → 它只把调用方给出的内容装进 OverrideTurnContext → 出来的是一条“把这些环境设置改掉”的命令。

调用关系:当用户或系统需要在不直接发新消息的情况下调整运行环境时会用它;后面的处理者会读取这些可选字段,只改有值的部分。

AppCommand::exec_approval195–205 ↗
fn exec_approval(
        id: String,
        turn_id: Option<String>,
        decision: CommandExecutionApprovalDecision,
    ) -> Self

作用:创建一个“用户是否批准执行命令”的答复。比如助手想运行一条危险命令时,系统先问用户,用户的选择就靠它传回去。

数据流:进去审批请求 id、可选的对话回合 id、以及批准或拒绝的决定 → 它把这些放进 ExecApproval → 出来的是一条针对某个执行请求的审批结果。

调用关系:审批界面收到用户选择后会调用它;后台根据 id 找到原来的命令执行请求,再按 decision 继续或取消。

AppCommand::patch_approval207–209 ↗
fn patch_approval(id: String, decision: FileChangeApprovalDecision) -> Self

作用:创建一个“用户是否批准修改文件”的答复。它专门用于文件改动,也就是补丁或编辑操作的审批。

数据流:进去文件修改请求 id 和用户决定 → 它生成 PatchApproval → 出来的是一条说明“这次文件改动允不允许”的命令。

调用关系:当助手准备改文件并需要用户确认时,用户的选择会通过它打包;后续处理者用 id 对上原请求,再执行或放弃修改。

AppCommand::resolve_elicitation211–225 ↗
fn resolve_elicitation(
        server_name: String,
        request_id: AppServerRequestId,
        decision: McpServerElicitationAction,
        content: Option<Value>,
        meta: Option<Value>,

作用:创建一个对外部 MCP 服务器提问的答复命令。MCP 可以理解成外接工具服务;elicitation 是服务向用户要更多信息。

数据流:进去服务器名、请求 id、用户决定、可选内容和可选附加信息 → 它们被装进 ResolveElicitation → 出来的是一条“这个外部服务的问题已经有答复了”的命令。

调用关系:当外部工具服务向用户追问信息时,界面拿到用户回应后调用它;后台再把决定和内容送回对应的服务器请求。

AppCommand::user_input_answer227–229 ↗
fn user_input_answer(id: String, response: ToolRequestUserInputResponse) -> Self

作用:创建一个“用户回答了工具提出的问题”的命令。它用于把用户补充输入交回给等待中的工具请求。

数据流:进去问题或请求的 id,以及用户的回答内容 → 它生成 UserInputAnswer → 出来的是一条可被后台匹配到原请求的回答命令。

调用关系:某个工具需要用户填写信息时,界面收到回答后会调用它;处理流程用 id 找到等待中的工具请求并继续。

AppCommand::request_permissions_response231–236 ↗
fn request_permissions_response(
        id: String,
        response: RequestPermissionsResponse,
    ) -> Self

作用:创建一个“权限请求答复”命令。也就是当系统请求某些权限时,把用户或界面的回应传回去。

数据流:进去权限请求 id 和具体回应 → 它生成 RequestPermissionsResponse → 出来的是一条说明权限是否给、给哪些的答复。

调用关系:权限确认流程结束后会调用它;后台根据 id 对应到原权限请求,再更新接下来能做什么。

AppCommand::reload_user_config238–240 ↗
fn reload_user_config() -> Self

作用:创建一个“重新读取用户配置”的命令。用户改了配置文件后,可以用它让程序重新加载设置。

数据流:进去不需要参数 → 它生成 ReloadUserConfig → 出来的是一条要求刷新配置的命令。

调用关系:界面或热重载机制会发出它;真正读取配置文件、应用新设置的工作由后续处理者完成。

AppCommand::list_skills242–244 ↗
fn list_skills(cwds: Vec<PathBuf>, force_reload: bool) -> Self

作用:创建一个“列出技能”的命令。这里的技能可以理解成项目或目录下可用的辅助能力、模板或工具说明。

数据流:进去一组工作目录 cwds,以及是否强制重新加载的标记 → 它生成 ListSkills → 出来的是一条要求扫描或展示技能列表的命令。

调用关系:当界面需要展示可用技能时会调用它;后台会根据目录和 force_reload 决定是用缓存还是重新读取。

AppCommand::compact246–248 ↗
fn compact() -> Self

作用:创建一个“压缩当前对话或上下文”的命令。它通常用于让长对话变短,保留重点,减少后续处理负担。

数据流:进去不需要参数 → 它生成 Compact → 出来的是一条要求整理压缩上下文的命令。

调用关系:当对话太长或用户主动要求整理时会用它;后续流程负责真正总结、缩短上下文。

AppCommand::set_thread_name250–252 ↗
fn set_thread_name(name: String) -> Self

作用:创建一个“设置线程名称”的命令。这里的线程更像一段对话或工作会话,不一定是操作系统里的线程。

数据流:进去一个名称字符串 → 它生成 SetThreadName,并把名称放进去 → 出来的是一条要求改名的命令。

调用关系:用户给当前会话改名时会调用它;后台收到后把对应会话的显示名或记录名更新掉。

AppCommand::shutdown255–257 ↗
fn shutdown() -> Self

作用:创建一个“关闭应用”的命令。它用于让主流程有秩序地停下来,而不是突然退出。

数据流:进去不需要参数 → 它生成 Shutdown → 出来的是一条关闭请求。

调用关系:虽然这里标了允许暂时不用,但它可以被退出流程调用;后续处理者收到后会做收尾和停止运行。

AppCommand::thread_rollback259–261 ↗
fn thread_rollback(num_turns: u32) -> Self

作用:创建一个“回滚若干轮对话”的命令。它用于撤回最近几轮内容,让会话退回到更早的状态。

数据流:进去要回滚的轮数 num_turns → 它生成 ThreadRollback → 出来的是一条说明要退回几轮的命令。

调用关系:用户想撤销最近对话时会用它;后台根据轮数修改会话历史。

AppCommand::review263–265 ↗
fn review(target: ReviewTarget) -> Self

作用:创建一个“请求审查”的命令。审查目标可能是某段改动、某个文件或其他 ReviewTarget 指定的对象。

数据流:进去一个审查目标 target → 它生成 Review → 出来的是一条要求对该目标做审查的命令。

调用关系:当用户要求检查代码或改动时会调用它;后续处理流程识别到 Review 命令后进入审查相关流程。

AppCommand::approve_guardian_denied_action267–269 ↗
fn approve_guardian_denied_action(event: GuardianAssessmentEvent) -> Self

作用:创建一个“批准 Guardian 原本拒绝的动作”的命令。Guardian 可以理解成安全看门人,它会拦住风险操作。

数据流:进去一个 GuardianAssessmentEvent,也就是看门人之前判断并拒绝的事件 → 它生成 ApproveGuardianDeniedAction → 出来的是一条表示用户或上层同意继续的命令。

调用关系:当安全机制拦下操作但用户决定放行时会调用它;后台根据事件信息继续处理那个被拒绝的动作。

AppCommand::is_review271–273 ↗
fn is_review(&self) -> bool

作用:检查当前命令是不是“审查”命令。它让调用方不用手动拆开枚举,就能快速判断。

数据流:进去当前 AppCommand 自身 → 它用 matches! 这个模式匹配工具看它是否是 Review → 出来 true 或 false,不改动命令内容。

调用关系:需要把审查命令单独分流的代码会调用它;它不再交给别的业务函数,只用语言自带的匹配能力做判断。

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

AppCommand::from277–279 ↗
fn from(value: &AppCommand) -> Self

作用:把一个 AppCommand 的引用复制成一个新的 AppCommand。简单说,就是拿着原件再复印一份。

数据流:进去一个 AppCommand 的引用 → 它调用 clone 复制里面的数据 → 出来一个拥有自己所有权的新 AppCommand,原来的命令不受影响。

调用关系:当某处只有命令的借用版本、但后续流程需要一个独立命令时会用到它;它把实际复制工作交给 clone。

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

tui/src/app/input.rs源码 ↗
orchestrationmain loop / keyboard event handling

这个文件像一个“前台接待员”。用户每按一次键,都会先来到这里。它先判断当前界面是不是有弹窗、覆盖层、输入框内容等特殊状态,避免把本该用于打字或移动光标的按键误当成全局快捷键。比如草稿不为空时,Alt+左右更像是在输入框里按词移动,而不是切换 agent 会话。它还负责打开外部编辑器:先找用户配置的编辑器,临时把终端恢复到适合普通编辑器运行的状态,编辑完再把文字放回聊天框。它也处理清屏、原始输出模式、查看完整 transcript(对话记录)、Esc 回退等操作。整体重点是“谁该吃掉这个按键”:全局功能优先,但不能抢走弹窗、输入框、Vim 模式等局部界面的正常行为。

函数细节11
App::launch_external_editor10–54 ↗
async fn launch_external_editor(&mut self, tui: &mut tui::Tui)

作用:真正启动用户外部编辑器的函数。它让用户可以用自己熟悉的编辑器写长消息,而不是只在终端输入框里编辑。

数据流:进去的是当前应用状态和 TUI 对象;它先读取环境里的编辑器命令,比如 $VISUAL 或 $EDITOR,再把聊天输入框里已有的草稿拿出来当初始内容。然后它临时恢复终端状态,运行外部编辑器。编辑成功后,它去掉末尾多余空白,把新文本放回聊天输入框;如果找不到编辑器或启动失败,就往聊天历史里加一条错误提示。最后它重置外部编辑器状态,并请求界面重画。

调用关系:它会调用 resolve_editor_command 找编辑器,调用 TUI 的 with_restored 临时让终端适合外部程序运行,并用 reset_external_editor_state 收尾。这个函数本身不是普通按键立刻直接做完的小动作,更像是外部编辑器流程里的执行阶段。

调用图:调用 2 个内部函数(reset_external_editor_state, resolve_editor_command);外部调用 4 个(frame_requester, with_restored, format!, new_error_event)。

App::request_external_editor_launch56–64 ↗
fn request_external_editor_launch(&mut self, tui: &mut tui::Tui)

作用:把“我要打开外部编辑器”这件事登记下来,并让界面显示相应提示。它不直接打开编辑器,而是先把状态切到“已请求”。

数据流:进去的是应用状态和 TUI;它把聊天组件里的外部编辑器状态设为 Requested,并在底部提示栏显示外部编辑器提示。之后它请求下一帧重画,让用户马上看到状态变化。

调用关系:它由 App::handle_key_event 在用户按下打开外部编辑器快捷键时调用。它做的是启动前的准备工作,真正运行编辑器的活由 App::launch_external_editor 完成。

调用图:被 1 处调用(handle_key_event);外部调用 2 个(frame_requester, vec!)。

App::reset_external_editor_state66–71 ↗
fn reset_external_editor_state(&mut self, tui: &mut tui::Tui)

作用:把外部编辑器相关状态恢复成“没有打开”的正常状态。它用于编辑器结束后,或启动失败后清理界面提示。

数据流:进去的是应用状态和 TUI;它把聊天组件里的外部编辑器状态设为 Closed,清掉底部提示栏的临时提示,并要求界面重画。出来的结果是界面回到普通聊天输入状态。

调用关系:它被 App::launch_external_editor 调用,用来保证不管编辑器成功、失败,应用都不会一直停留在“正在打开编辑器”的状态。

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

App::apply_raw_output_mode73–90 ↗
fn apply_raw_output_mode(
        &mut self,
        tui: &mut tui::Tui,
        enabled: bool,
        notify: bool,
    )

作用:切换“原始输出模式”。可以理解为改变聊天记录显示方式:更偏向按原始内容展示,而不是普通美化后的显示。

数据流:进去的是应用状态、TUI、是否开启 enabled,以及要不要通知用户 notify。它先改聊天组件里的显示模式;如果需要通知,就用带提示的方式改。然后它立刻重新排版 transcript(对话记录),因为显示规则变了,旧排版可能不对。排版失败时会记录警告并在界面显示错误。最后请求重画。

调用关系:它由 App::handle_key_event 在用户按下切换原始输出的快捷键时调用。它自己还会触发 transcript 重排,把模式变化真正反映到屏幕上。

调用图:被 1 处调用(handle_key_event);外部调用 3 个(frame_requester, format!, warn!)。

App::handle_key_event92–255 ↗
async fn handle_key_event(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        key_event: KeyEvent,
    )

作用:这是本文件最核心的按键分发器。每次用户按键,它决定这个键是触发全局功能,还是交给聊天输入框、弹窗、覆盖层等界面处理。

数据流:进去的是应用状态、TUI、后台会话 AppServerSession,以及一个具体按键 KeyEvent。它先检查是否能用快捷键切换前后 agent 会话,再检查是否从侧边会话返回。然后判断全局快捷键是否可用,比如切换 Vim 模式、快速模式、原始输出、打开 transcript、请求外部编辑器。遇到 Esc 时,它会判断是进入回退流程、拒绝侧边回退,还是交给聊天组件。遇到 Ctrl-L 类清屏快捷键,它会清理终端界面并重画。没有被全局逻辑处理的按键,最后都会交给 chat_widget 处理。

调用关系:它站在整个键盘输入流程的入口位置。它会调用 app_keymap_shortcuts_available 判断全局快捷键能不能生效,调用 request_external_editor_launch、apply_raw_output_mode 等函数完成具体动作,也会调用 should_handle_backtrack_esc、should_reject_side_backtrack_esc、reject_side_backtrack_esc 处理 Esc 回退。需要显示完整记录时,它会创建 transcript 覆盖层并让 TUI 进入备用屏幕。

调用图:调用 7 个内部函数(app_keymap_shortcuts_available, apply_raw_output_mode, reject_side_backtrack_esc, request_external_editor_launch, should_handle_backtrack_esc, should_reject_side_backtrack_esc, new_transcript);外部调用 5 个(enter_alt_screen, frame_requester, format!, matches!, warn!)。

App::should_handle_backtrack_esc257–262 ↗
fn should_handle_backtrack_esc(&self, key_event: KeyEvent) -> bool

作用:判断这次 Esc 是否应该启动或推进“回退到之前消息”的操作。它专门避免在输入、侧边会话或 Vim 插入模式里误触发回退。

数据流:进去的是当前应用状态和按键事件;它检查是否没有侧边会话、聊天组件处于普通回退模式、输入框为空,并且这个 Esc 不该被 Vim 插入模式自己处理。满足这些条件就返回 true,否则返回 false。

调用关系:它被 App::handle_key_event 在遇到 Esc 时调用。它只是做判断,不改状态;如果返回 true,后续由 handle_key_event 去调用真正处理回退的函数。

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

App::should_reject_side_backtrack_esc264–269 ↗
fn should_reject_side_backtrack_esc(&self, key_event: KeyEvent) -> bool

作用:判断用户在侧边会话里按 Esc 时,是否应该提示“这里不能回退编辑上一条”。这是为了防止用户以为侧边会话也能执行主线回退。

数据流:进去的是当前应用状态和按键事件;它检查侧边会话是否正在进行、是否是普通回退模式、输入框是否为空,以及 Vim 插入模式是否不需要自己处理这个 Esc。满足条件就返回 true,表示应该拒绝这次侧边回退。

调用关系:它被 App::handle_key_event 在处理 Esc 时调用。如果它返回 true,handle_key_event 会接着调用 App::reject_side_backtrack_esc 来清状态并显示错误。

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

App::reject_side_backtrack_esc271–275 ↗
fn reject_side_backtrack_esc(&mut self)

作用:在侧边会话里用户尝试用 Esc 回退时,明确拒绝这次操作,并告诉用户为什么不行。

数据流:进去的是应用状态;它先重置已有的回退状态,避免留下半吊子的 Esc 准备状态,然后往聊天界面加一条错误消息。出来的效果是用户看到提示,应用也回到安全状态。

调用关系:它由 App::handle_key_event 在 should_reject_side_backtrack_esc 判断为 true 后调用。它是侧边会话回退限制的实际执行者。

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

App::app_keymap_shortcuts_available277–279 ↗
fn app_keymap_shortcuts_available(&self) -> bool

作用:判断全局快捷键现在能不能用。简单说,就是有覆盖层、弹窗或模态界面时,先别让全局快捷键抢按键。

数据流:进去的是当前应用状态;它检查 overlay 是否为空,以及聊天组件是否没有打开模态窗口或弹窗。两个条件都满足就返回 true,否则返回 false。

调用关系:它被 App::handle_key_event 多次调用,用来保护局部界面的按键体验。测试函数也专门验证了打开快捷键查看界面时,这个判断会变成 false。

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

App::refresh_status_line281–283 ↗
fn refresh_status_line(&mut self)

作用:刷新底部状态行。状态行通常显示当前模式、提示或一些运行状态,刷新它能让界面信息保持最新。

数据流:进去的是应用状态;它把刷新请求转交给 chat_widget。出来的效果是聊天组件更新自己的状态行内容。

调用关系:这是一层很薄的转发函数。调用清单里没有显示谁调用它,但它的作用是在应用层需要更新状态栏时,把事情交给聊天组件去做。

tests::app_keymap_shortcuts_are_disabled_while_keymap_view_is_active291–299 ↗
async fn app_keymap_shortcuts_are_disabled_while_keymap_view_is_active()

作用:这是一个自动测试,确认打开快捷键查看界面时,全局快捷键会被禁用。这样用户在查看快捷键说明时,不会一按键就触发别的全局动作。

数据流:进去的是测试环境;它先创建一个测试用 App,确认一开始全局快捷键可用。然后打开快捷键调试界面,再检查 app_keymap_shortcuts_available 变成 false。测试通过说明这个保护逻辑正常。

调用关系:它调用 make_test_app 搭好测试应用,并用断言检查结果。它验证的是 App::app_keymap_shortcuts_available 这条规则,间接保护 App::handle_key_event 的按键分发行为。

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

应用服务器事件与请求处理

这些文件将协议级应用服务器流量桥接到 TUI 管理的事件、待处理审批和序列化的用户响应。

tui/src/app/app_server_events.rs源码 ↗
orchestrationmain loop / event handling

这个文件解决的是前台文字界面和后台 app-server 之间的沟通问题。服务器会不断发来事件:有的是普通通知,比如账号状态变了、插件列表更新了;有的是需要用户处理的请求;也可能是事件太多导致漏收,或者连接断开。这里的 App 方法会先判断事件类型,再决定怎么处理。和某个对话线程有关的消息,会被放进对应线程的队列;属于整个应用的消息,会直接更新界面;不支持或没有线程编号的请求,会被拒绝或忽略并记录警告。它还特别处理 MCP 启动服务器列表、账号限额、外部代理配置导入完成等情况。简单说,它是服务器事件进入 TUI 后的“交通指挥岗”,保证消息不会乱跑,也让用户能看到及时、正确的状态。

函数细节4
App::refresh_mcp_startup_expected_servers_from_config18–28 ↗
fn refresh_mcp_startup_expected_servers_from_config(&mut self)

作用:这个函数从当前配置里找出启用的 MCP 服务器,并告诉聊天界面:启动时应该等哪些服务器准备好。MCP 可以理解成让应用连接外部工具或服务的一套接口。

数据流:进去的是 App 里已经加载好的配置。它读取配置中的 mcp_servers,筛出 enabled 为真的服务器名字,组成一个名字列表;然后把这个列表交给 chat_widget。结果是界面知道哪些 MCP 服务器是本次启动应该期待的对象。

调用关系:当事件流落后、可能漏掉 MCP 状态消息时,App::handle_app_server_event 会调用它来重新对齐界面状态;当服务器通知 MCP 状态更新时,App::handle_server_notification_event 也会调用它,保证界面显示的期待列表跟配置一致。

调用图:被 2 处调用(handle_app_server_event, handle_server_notification_event)。

App::handle_app_server_event30–58 ↗
async fn handle_app_server_event(
        &mut self,
        app_server_client: &AppServerSession,
        event: AppServerEvent,
    )

作用:这是服务器事件进入 TUI 后的总入口。它先看事件属于哪一类,再把具体工作交给处理通知或处理请求的函数,或者在断线时要求应用退出。

数据流:进去的是一个 app_server_client 和一个 AppServerEvent。它根据事件类型分流:如果事件漏收,就记录警告、刷新 MCP 期待列表并结束启动等待;如果是服务器通知,就交给 App::handle_server_notification_event;如果是服务器请求,就交给 App::handle_server_request_event;如果断线,就在界面显示错误,并发送 FatalExitRequest 让应用走致命退出流程。出来没有普通返回值,但会更新界面、写日志、发送应用内部事件。

调用关系:它是本文件的上层分发员,运行时每收到一个后台服务器事件都会经过这里。它不会自己处理所有细节,而是把通知交给 App::handle_server_notification_event,把请求交给 App::handle_server_request_event;遇到漏收时会调用 App::refresh_mcp_startup_expected_servers_from_config 来修正界面预期。

调用图:调用 3 个内部函数(handle_server_notification_event, handle_server_request_event, refresh_mcp_startup_expected_servers_from_config);外部调用 2 个(FatalExitRequest, warn!)。

App::handle_server_notification_event60–173 ↗
async fn handle_server_notification_event(
        &mut self,
        app_server_client: &AppServerSession,
        notification: ServerNotification,
    )

作用:这个函数处理服务器主动发来的“通知”。通知通常不是要用户马上回答,而是告诉界面某些状态变了,比如账号、限额、插件、MCP 服务器、某个请求已完成等。

数据流:进去的是 app_server_client 和一条 ServerNotification。它先处理几类特殊通知:请求已解决就从待处理列表里移除并让界面关闭提示;MCP 状态变化就刷新期待列表;账号限额和账号信息会直接更新聊天界面;外部代理配置导入完成后会重新读配置、刷新插件提及、重新加载用户配置并拉取插件列表;应用列表更新会把连接器快照送到界面。若不是这些特殊情况,它会判断通知属于某个线程、整个应用还是全局消息:线程消息会进对应队列,全局消息会直接交给 chat_widget 显示或处理。出来没有普通返回值,但会改变界面状态、队列状态和配置相关状态。

调用关系:它由 App::handle_app_server_event 在收到 ServerNotification 时调用。它会借助 server_notification_thread_target 判断通知该送去哪儿;需要显示账号信息时调用 status_account_display_from_auth_mode;外部代理配置导入完成时还会让会话对象消费完成标记,并触发重新加载用户配置。它是通知从服务器落到具体界面位置的关键分拣点。

调用图:调用 4 个内部函数(server_notification_thread_target, refresh_mcp_startup_expected_servers_from_config, consume_external_agent_config_import_completion, status_account_display_from_auth_mode);被 1 处调用(handle_app_server_event);外部调用 4 个(reload_user_config, matches!, debug!, warn!)。

App::handle_server_request_event175–218 ↗
async fn handle_server_request_event(
        &mut self,
        app_server_client: &AppServerSession,
        request: ServerRequest,
    )

作用:这个函数处理服务器发来的“请求”。请求和通知不同,它通常表示服务器希望前台界面做出某种响应或展示给用户处理。

数据流:进去的是 app_server_client 和一条 ServerRequest。它先把请求登记到 pending_app_server_requests;如果发现是不支持的请求,就记录警告、在界面显示错误,并调用拒绝请求的流程告诉服务器这事办不了。若请求可支持,它再提取线程编号;没有线程编号的请求会被忽略并记录警告。拿到线程编号后,它把请求放进主线程队列或对应的其他线程队列。结果是可处理请求会进入正确对话线程等待后续处理,不可处理请求会被拒绝。

调用关系:它由 App::handle_app_server_event 在收到 ServerRequest 时调用。它依赖 server_request_thread_id 判断请求属于哪个对话线程;之后把请求交给主线程或指定线程的入队函数。这样服务器请求不会直接打断整个应用,而是被放到正确的对话上下文里。

调用图:调用 1 个内部函数(server_request_thread_id);被 1 处调用(handle_app_server_event);外部调用 1 个(warn!)。

tui/src/app/app_server_requests.rs源码 ↗
domain_logicrequest handling

app-server 会不断向 TUI 发请求,比如“能不能执行这个命令”“能不能改这个文件”“请用户填一下这个表”。这些请求都有服务器自己的 request_id,而界面里用户操作常用的是 item_id、approval_id、turn_id 等别的编号。这个文件的核心作用,就是把这些编号对上号,避免用户点了同意,却回给了另一个请求。PendingAppServerRequests 像一个前台登记本:收到请求时先按类型记下来;用户后来做出决定时,再查登记本,生成 app-server 能看懂的 JSON 结果;如果服务器通知某个请求已经结束,也会把它从登记本删掉。它还会提前拒绝 TUI 目前不支持的请求,比如动态工具调用,避免服务器一直傻等。用户输入请求比较特别:同一个 turn_id 下可能有多个问题,所以用队列,按先来先答的顺序处理。

函数细节19
App::reject_app_server_request18–35 ↗
async fn reject_app_server_request(
        &self,
        app_server_client: &AppServerSession,
        request_id: AppServerRequestId,
        reason: String,
    ) -> std::result::Result<(), String

作用:把某个 app-server 请求明确拒绝掉,并带上一段原因。这样服务器不会一直等 TUI 回答,也能知道失败原因。

数据流:输入是 app-server 会话、服务器请求编号和拒绝原因 → 它把原因包装成 JSON-RPC 错误对象,错误码固定为 -32000 → 调用会话的 reject_server_request 发回服务器;成功时什么也不返回,失败时返回一段可读的错误文字。

调用关系:当 TUI 收到自己不能处理或需要主动拒绝的服务器请求时,会用这个函数收尾。它把真正的网络/协议发送工作交给 AppServerSession 的 reject_server_request。

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

PendingAppServerRequests::clear80–86 ↗
fn clear(&mut self)

作用:清空所有还没处理完的 app-server 请求记录。常用于会话重置或断开时,避免旧请求混进新流程。

数据流:输入是这本“登记簿”本身 → 它把命令审批、文件修改审批、权限审批、用户输入、MCP 请求这几类记录全部清掉 → 之后登记簿变成空的,没有返回值。

调用关系:它是一个清场工具,不把工作交给别的函数。通常在上层 App 需要重置待处理状态时调用。

PendingAppServerRequests::note_server_request88–171 ↗
fn note_server_request(
        &mut self,
        request: &ServerRequest,
    ) -> Option<UnsupportedAppServerRequest>

作用:收到 app-server 新请求时,把它登记下来,方便之后用户操作能找到对应的服务器 request_id。遇到 TUI 不支持的请求,会立刻给出“不能处理”的说明。

数据流:输入是一个 ServerRequest,也就是服务器发来的请求 → 它按请求类型挑一个合适的表保存编号:命令审批保存 approval_id 或 item_id,文件和权限保存 item_id,用户输入按 turn_id 排队,MCP 请求按服务器名和 request_id 保存;权限请求还会先检查路径能不能变成本机路径 → 如果能处理就返回 None,如果不支持或权限路径非法,就返回 UnsupportedAppServerRequest,里面带 request_id 和原因。

调用关系:这是登记簿的入口。上层收到服务器请求后会先调用它;如果它返回不支持,上层通常会再用 App::reject_app_server_request 告诉服务器。它内部会调用外部的 try_from 做权限路径校验,并用 format! 拼错误信息。

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

PendingAppServerRequests::take_resolution173–273 ↗
fn take_resolution(
        &mut self,
        op: T,
    ) -> Result<Option<AppServerRequestResolution>, String>

作用:用户已经在 TUI 里做出选择后,用这个函数把界面里的操作变成 app-server 需要的回复。它还会顺手把对应的待处理请求从登记簿里移走。

数据流:输入是一个可以转成 AppCommand 的操作,比如同意执行命令、拒绝改文件、回答权限请求、回答用户输入、解决 MCP 询问 → 它先转成统一的 AppCommand,再按命令类型查之前登记的 request_id,并把用户决定序列化成 JSON 值 → 输出是可发送给 app-server 的 AppServerRequestResolution;如果找不到对应待处理请求,输出 None;如果 JSON 转换失败,输出错误字符串。

调用关系:这是“用户操作 → 服务器回复”的桥。命令、文件、权限、MCP 直接查表;用户输入会交给 pop_user_input_request_for_turn,从同一个 turn_id 的队列里取最早的一个。

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

PendingAppServerRequests::resolve_notification275–325 ↗
fn resolve_notification(
        &mut self,
        request_id: &AppServerRequestId,
    ) -> Option<ResolvedAppServerRequest>

作用:当 app-server 通知某个请求已经结束时,用 request_id 找到它原来对应界面里的哪个东西,并从登记簿删除。这样界面不会继续显示一个已经无效的待办项。

数据流:输入是服务器的 request_id → 它依次在命令审批、文件审批、权限审批、用户输入、MCP 请求这些记录里查找 → 找到后删除记录,并返回 ResolvedAppServerRequest,告诉外面具体是哪类界面项被解决;找不到就返回 None。

调用关系:这是“服务器主动通知结束”这条路的收尾函数。查普通表时自己处理;查用户输入时交给 remove_user_input_request,因为用户输入藏在按 turn_id 分组的队列里。

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

PendingAppServerRequests::contains_server_request327–358 ↗
fn contains_server_request(&self, request: &ServerRequest) -> bool

作用:检查某个服务器请求现在是不是已经在登记簿里。它用来避免重复登记,或者判断一个请求是否还在等待处理。

数据流:输入是一个 ServerRequest → 它根据请求类型,在对应的待处理记录里找相同的 request_id;用户输入会遍历每个 turn_id 下的队列 → 输出 true 或 false;对于当前 TUI 不真正登记的几类请求,它直接认为 true,避免被当成漏掉的新请求重复处理。

调用关系:它是一个查询函数,不修改状态,也不调用别的本地函数。上层流程可以在接到请求或刷新状态时用它判断“这个我是不是已经知道了”。

PendingAppServerRequests::pop_user_input_request_for_turn360–376 ↗
fn pop_user_input_request_for_turn(
        &mut self,
        turn_id: &str,
    ) -> Option<PendingUserInputRequest>

作用:从某个对话轮次 turn_id 下,取出最早等待回答的用户输入请求。它保证同一轮里多个输入请求按先来先服务处理。

数据流:输入是 turn_id → 它找到这个 turn_id 对应的队列,从队头拿走一个 PendingUserInputRequest;如果拿完后队列空了,就把这个 turn_id 也删掉 → 输出被取出的用户输入请求,或者没有找到时输出 None。

调用关系:它只被 PendingAppServerRequests::take_resolution 调用。用户在界面提交某个 turn_id 的回答时,take_resolution 用它找到该回给服务器的那个 request_id。

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

PendingAppServerRequests::remove_user_input_request378–394 ↗
fn remove_user_input_request(
        &mut self,
        request_id: &AppServerRequestId,
    ) -> Option<PendingUserInputRequest>

作用:按服务器 request_id 精确删除一个用户输入请求。它用于服务器那边说“这个请求已经结束了”,而不是用户主动回答。

数据流:输入是 request_id → 它在所有 turn_id 的用户输入队列里查找这个编号,找到后从队列中间移除;如果队列空了,也删除这个 turn_id → 输出被移除的 PendingUserInputRequest,找不到则输出 None。

调用关系:它只被 PendingAppServerRequests::resolve_notification 调用。因为服务器通知只带 request_id,所以 resolve_notification 需要它从分组队列里反查并清理。

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

tests::resolves_exec_approval_through_app_server_request_id445–480 ↗
fn resolves_exec_approval_through_app_server_request_id()

作用:测试命令执行审批能不能正确从界面里的 approval_id 对回 app-server 的 request_id。重点是防止“同意命令”回错请求。

数据流:测试先创建空登记簿,再登记一个命令执行审批请求,request_id 是 41、approval_id 是 approval-1 → 然后模拟用户接受 approval-1 → 期望拿到的回复仍然指向 request_id 41,JSON 内容是 decision: accept。

调用关系:这个测试覆盖 note_server_request 和 take_resolution 的命令审批路径。它用断言确认登记和取回复这两步能连起来。

调用图:外部调用 3 个(Integer, assert_eq!, default)。

tests::rejects_permissions_with_paths_that_cannot_be_localized483–528 ↗
fn rejects_permissions_with_paths_that_cannot_be_localized()

作用:测试权限请求里如果带了不能变成本机绝对路径的文件路径,会被马上拒绝登记。这样服务器不会等一个之后无法干净回答的请求。

数据流:测试构造一个包含相对路径 relative/path 的权限请求 → note_server_request 尝试把权限路径转换成本机路径时失败 → 期望返回 UnsupportedAppServerRequest,并且错误信息说明文件系统路径本地化失败。

调用关系:这个测试覆盖 note_server_request 里的权限预检查分支。它也间接确认外部 try_from 的错误会被包装成给 app-server 的拒绝原因。

调用图:调用 1 个内部函数(try_from);外部调用 7 个(Integer, from, try_from, assert_eq!, cfg!, default, vec!)。

tests::resolves_permissions_and_user_input_through_app_server_request_id531–662 ↗
fn resolves_permissions_and_user_input_through_app_server_request_id()

作用:测试权限审批和用户输入这两种请求,都能从界面操作正确变成 app-server 回复。它同时检查权限内容转换是否符合协议要求。

数据流:测试先登记一个权限请求和一个用户输入请求 → 然后模拟用户授予网络和文件读写权限,检查输出 request_id 是原来的 7,JSON 能解回正确的权限回复 → 再模拟用户回答问题,检查输出 request_id 是原来的 8,回答内容没有丢失。

调用关系:这个测试覆盖 note_server_request 和 take_resolution 的权限路径、用户输入路径。用户输入部分会走到 pop_user_input_request_for_turn。

调用图:调用 1 个内部函数(from_read_write_roots);外部调用 5 个(assert_eq!, cfg!, once, default, vec!)。

tests::correlates_mcp_elicitation_server_request_with_resolution665–710 ↗
fn correlates_mcp_elicitation_server_request_with_resolution()

作用:测试 MCP elicitation 请求能按服务器名和 request_id 正确配对。MCP elicitation 可以理解为外部工具服务器向用户追问信息。

数据流:测试登记一个来自 example 服务器、request_id 为 12 的 MCP 表单请求 → 然后模拟用户接受并填写内容和元数据 → 期望生成的回复仍然回到 request_id 12,JSON 里包含 action、content 和 _meta。

调用关系:这个测试覆盖 note_server_request 和 take_resolution 的 MCP 路径,确认 McpRequestKey 这个复合钥匙能正确找到原请求。

调用图:外部调用 4 个(Integer, assert_eq!, json!, default)。

tests::rejects_dynamic_tool_calls_as_unsupported713–734 ↗
fn rejects_dynamic_tool_calls_as_unsupported()

作用:测试 TUI 目前不支持动态工具调用时,会给出明确拒绝原因。这样新协议请求不会被默默吞掉。

数据流:测试构造一个 DynamicToolCall 请求 → note_server_request 发现 TUI 还不能处理它 → 输出 UnsupportedAppServerRequest,里面保留 request_id 99,并写明动态工具调用暂不可用。

调用关系:这个测试只针对 note_server_request 的不支持分支。它保证上层可以拿到拒绝信息,再交给 App::reject_app_server_request 发回服务器。

调用图:外部调用 4 个(Integer, assert_eq!, json!, default)。

tests::does_not_mark_chatgpt_auth_refresh_as_unsupported737–750 ↗
fn does_not_mark_chatgpt_auth_refresh_as_unsupported()

作用:测试 ChatGPT 认证令牌刷新请求不会被误判成“不支持”。这类请求虽然不登记审批,但也不该被拒绝。

数据流:测试创建一个认证刷新请求 → 调用 note_server_request → 期望返回 None,表示不用登记,也不用拒绝。

调用关系:这个测试覆盖 note_server_request 的 ChatgptAuthTokensRefresh 分支,防止认证刷新流程被 TUI 错误挡住。

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

tests::resolves_patch_approval_through_app_server_request_id753–780 ↗
fn resolves_patch_approval_through_app_server_request_id()

作用:测试文件修改审批能正确回到原来的 app-server request_id。这里的 patch 可以理解为一次文件改动申请。

数据流:测试登记一个 FileChangeRequestApproval,item_id 是 patch-1,request_id 是 13 → 然后模拟用户取消这次修改 → 期望生成的回复指向 request_id 13,JSON 内容是 decision: cancel。

调用关系:这个测试覆盖 note_server_request 和 take_resolution 的文件修改审批路径。它确认 item_id 和服务器 request_id 的映射没有弄错。

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

tests::resolve_notification_returns_resolved_exec_request783–818 ↗
fn resolve_notification_returns_resolved_exec_request()

作用:测试服务器通知某个命令审批结束时,登记簿能返回对应的界面审批 id,并且只返回一次。

数据流:测试先登记一个命令审批,approval_id 是 approval-1,request_id 是 41 → 第一次用 request_id 41 调 resolve_notification,期望得到 ExecApproval 和 approval-1 → 第二次再调同一个 request_id,期望得到 None,因为记录已经删掉。

调用关系:这个测试覆盖 resolve_notification 的命令审批分支,确认它既能反查 id,也会清理状态。

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

tests::resolve_notification_returns_resolved_mcp_request821–852 ↗
fn resolve_notification_returns_resolved_mcp_request()

作用:测试服务器通知 MCP 请求结束时,登记簿能告诉外面是哪一个 MCP 服务器的哪一个请求结束了。

数据流:测试登记一个 example 服务器发来的 MCP 请求,request_id 是 12 → 调用 resolve_notification → 期望得到 McpElicitation,里面带 server_name example 和 request_id 12。

调用关系:这个测试覆盖 resolve_notification 的 MCP 分支,确认复合键保存的信息能在通知结束时还原出来。

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

tests::resolve_notification_returns_resolved_user_input_item_id855–874 ↗
fn resolve_notification_returns_resolved_user_input_item_id()

作用:测试服务器通知用户输入请求结束时,登记簿能返回界面里对应的工具调用 item_id。这样界面可以把正确的输入框移除或标记结束。

数据流:测试登记一个用户输入请求,turn_id 是 turn-1,item_id 是 tool-1,request_id 是 8 → 调用 resolve_notification → 期望得到 UserInput,call_id 是 tool-1。

调用关系:这个测试覆盖 resolve_notification 的用户输入分支。该分支内部会调用 remove_user_input_request,从按 turn_id 分组的队列里按 request_id 删除。

调用图:外部调用 4 个(Integer, new, assert_eq!, default)。

tests::same_turn_user_input_answers_resolve_app_server_requests_fifo877–912 ↗
fn same_turn_user_input_answers_resolve_app_server_requests_fifo()

作用:测试同一个 turn_id 下如果有多个用户输入请求,回答会按先进先出的顺序对应到服务器请求。先进先出就是先来的先处理,像排队买票。

数据流:测试给同一个 turn-1 依次登记 request_id 8 和 9 两个用户输入请求 → 连续提交两次同一个 turn_id 的回答 → 期望第一次回复 request_id 8,第二次回复 request_id 9。

调用关系:这个测试覆盖 take_resolution 调用 pop_user_input_request_for_turn 的队列行为,确保同一轮多个输入不会乱序。

调用图:外部调用 5 个(Integer, new, new, assert_eq!, default)。

线程路由与交互重放

这些文件管理每个线程的状态,路由线程范围的命令和事件,并在线程切换时保留未解决的交互式提示。

tui/src/app/pending_interactive_replay.rs源码 ↗
domain_logiccross-cutting:收到服务器请求、发出用户回答、线程事件缓存重放或清理时都会用到

终端界面会把线程里的事件缓存起来,切换回来时再像放录像一样重放。但有些事件不能无脑重放,比如“是否允许执行命令”“是否允许改文件”“请用户输入”“MCP 服务器提问”。如果这些提示已经被回答,再显示一次就会让用户误操作。这个文件里的 PendingInteractiveReplayState 就像一张待办清单:收到服务器请求时把提示记上;用户发出回答、服务器说请求已解决、某一轮对话结束、事件被缓存挤掉、线程关闭时,再把对应提示划掉。它同时按提示编号快速查找,也按 turn_id(一轮对话的编号)排队保存,方便一次清掉一轮里的过期提示。特别的是用户输入提示按先进先出处理,因为回答只带 turn_id,不带具体提示编号,所以默认回答最早排队的那个。

函数细节38
ElicitationRequestKey::new16–21 ↗
fn new(server_name: String, request_id: AppServerRequestId) -> Self

作用:把 MCP 服务器名和请求编号合成一个唯一钥匙。这样即使不同服务器用了相似编号,也能分清是哪一个提问。

数据流:进去的是 server_name 和 request_id → 函数把它们放进 ElicitationRequestKey 这个小结构 → 出来的是可放进集合里查重、删除、匹配的钥匙。

调用关系:当记录、删除或判断 MCP elicitation(服务器向用户追问信息)时会用它;note_server_request、note_outbound_op、note_evicted_server_request 和 should_replay_snapshot_request 都靠这把钥匙找到同一个请求。

调用图:被 4 处调用(note_evicted_server_request, note_outbound_op, note_server_request, should_replay_snapshot_request)。

PendingInteractiveReplayState::op_can_change_state73–87 ↗
fn op_can_change_state(op: T) -> bool

作用:快速判断一个发出去的操作会不会改变“待处理提示清单”。不相关的操作可以跳过,省得白做状态更新。

数据流:进去的是某个可转成 AppCommand 的操作 → 函数把它转成统一命令类型,再看它是不是审批、回答、解决提问或关闭程序 → 出来的是 true 或 false。

调用关系:上层的 op_can_change_pending_replay_state 会先问它一句:这个操作值得通知待办清单吗;如果值得,后面才会让 note_outbound_op 真正改状态。

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

PendingInteractiveReplayState::note_outbound_op89–170 ↗
fn note_outbound_op(&mut self, op: T)

作用:记录“用户或界面已经发出了回答/批准/拒绝”这件事,并把对应的待处理提示从清单里删掉。没有它,已回答的弹窗在切换线程后可能又被重放。

数据流:进去的是一个 AppCommand,比如同意执行命令、同意改文件、回答用户输入、解决 MCP 提问或关闭 → 函数找到对应提示编号或请求钥匙,从快速集合、按轮次排队表、按请求编号表里一起删除 → 出来没有返回值,但内部待办状态变干净了;遇到 Shutdown 会全部清空。

调用关系:它由上层 note_outbound_op 在发送操作后调用;它会借助 ElicitationRequestKey::new 定位 MCP 请求,也会调用 clear 和两个 remove_call_id_from_turn_map 系列小工具做清理。

调用图:调用 2 个内部函数(new, clear);被 1 处调用(note_outbound_op);外部调用 3 个(remove_call_id_from_turn_map, remove_call_id_from_turn_map_entry, into)。

PendingInteractiveReplayState::note_server_request172–250 ↗
fn note_server_request(&mut self, request: &ServerRequest)

作用:服务器发来一个需要用户参与的请求时,把它登记为“还没处理”。这是待办清单增加内容的主要入口。

数据流:进去的是 ServerRequest → 函数按请求类型分门别类:命令执行审批、文件修改审批、MCP 提问、工具请求用户输入、权限请求都会被存进对应集合和按 turn_id 的队列,同时也用 request_id 建一份索引 → 出来没有返回值,但之后快照重放就知道这些提示还该显示。

调用关系:ThreadEventStore 的 push_request 收到服务器请求后会调用它;遇到 MCP 提问时会用 ElicitationRequestKey::new 生成唯一钥匙。

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

PendingInteractiveReplayState::note_server_notification252–283 ↗
fn note_server_notification(&mut self, notification: &ServerNotification)

作用:服务器发来状态通知时,用它更新待办清单。比如一轮完成了、请求被服务器标记解决了、线程关闭了,都要清掉相关提示。

数据流:进去的是 ServerNotification → 函数看通知种类:某个执行/改文件项目已开始就移除对应审批;一轮完成就清掉这一轮的审批和输入;请求已解决就按 request_id 删除;线程关闭就全清 → 出来没有返回值,但待重放的提示会减少。

调用关系:ThreadEventStore 的 push_notification 会在收到通知时调用它;它把具体清理工作交给 clear_exec_approval_turn、clear_patch_approval_turn、clear_request_permissions_turn、clear_request_user_input_turn、remove_request 或 clear。

调用图:调用 6 个内部函数(clear, clear_exec_approval_turn, clear_patch_approval_turn, clear_request_permissions_turn, clear_request_user_input_turn, remove_request);被 1 处调用(push_notification);外部调用 1 个(remove_call_id_from_turn_map)。

PendingInteractiveReplayState::note_evicted_server_request285–352 ↗
fn note_evicted_server_request(&mut self, request: &ServerRequest)

作用:当旧事件被缓存容量挤出去时,把对应的待处理记录也删掉。这样清单不会保存已经不在录像里的老提示。

数据流:进去的是即将被移出缓存的 ServerRequest → 函数按请求类型删除对应 call_id、turn_id 队列项、MCP 钥匙,并从 request_id 索引里删掉匹配项 → 出来没有返回值,但内部状态和事件缓存保持一致。

调用关系:push_request 或 push_notification 造成缓存淘汰旧请求时会调用它;它会用 ElicitationRequestKey::new 和 remove_call_id_from_turn_map_entry 做精确删除。

调用图:调用 1 个内部函数(new);被 2 处调用(push_notification, push_request);外部调用 1 个(remove_call_id_from_turn_map_entry)。

PendingInteractiveReplayState::should_replay_snapshot_request354–376 ↗
fn should_replay_snapshot_request(&self, request: &ServerRequest) -> bool

作用:决定某个服务器请求在生成快照时该不该重放。核心规则是:互动提示只有还没解决才重放,普通请求默认重放。

数据流:进去的是一个 ServerRequest → 函数检查它是否还在对应的待办集合里;MCP 请求会先生成服务器名加请求编号的钥匙再查 → 出来的是 true 或 false,告诉快照过滤器保留还是丢掉。

调用关系:它是线程快照过滤的判断器;虽然调用清单没有列出调用者,但它显然服务于 ThreadEventStore 的 snapshot 流程,用 ElicitationRequestKey::new 匹配 MCP 提问。

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

PendingInteractiveReplayState::has_pending_thread_approvals378–383 ↗
fn has_pending_thread_approvals(&self) -> bool

作用:告诉外面:当前线程里还有没有等待批准的事情,比如执行命令、改文件、MCP 提问或权限请求。用户输入不算审批。

数据流:进去的是当前状态本身 → 函数检查几个审批类集合是否为空 → 出来的是布尔值,true 表示还有审批类提示没处理。

调用关系:上层 has_pending_thread_approvals 和 side_parent_pending_status 会用它决定界面上是否显示“还有待批准事项”的状态。

调用图:被 2 处调用(has_pending_thread_approvals, side_parent_pending_status)。

PendingInteractiveReplayState::has_pending_thread_user_input385–387 ↗
fn has_pending_thread_user_input(&self) -> bool

作用:告诉外面:当前线程里还有没有等待用户填写答案的输入提示。它和审批分开算,避免把“填表”误当成“批准”。

数据流:进去的是当前状态本身 → 函数只看 request_user_input_call_ids 是否为空 → 出来的是布尔值,true 表示还有输入问题在等回答。

调用关系:side_parent_pending_status 会用它给父级或侧边栏显示“这个线程还在等用户输入”的状态。

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

PendingInteractiveReplayState::clear_request_user_input_turn389–400 ↗
fn clear_request_user_input_turn(&mut self, turn_id: &str)

作用:清掉某一轮对话里所有还没回答的用户输入提示。常见场景是一轮已经结束,旧问题就不该再弹出来。

数据流:进去的是 turn_id → 函数从按轮次保存的队列里取出这一轮所有 call_id,逐个从快速集合里删掉,再从 request_id 索引里删掉同一轮的用户输入请求 → 出来没有返回值,状态里这一轮的输入提示消失。

调用关系:note_server_notification 在收到 TurnCompleted(一轮完成)通知时会调用它,作为一轮结束清理的一部分。

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

PendingInteractiveReplayState::clear_request_permissions_turn402–413 ↗
fn clear_request_permissions_turn(&mut self, turn_id: &str)

作用:清掉某一轮对话里所有权限请求提示。这样一轮结束后,不会继续显示已经过期的权限申请。

数据流:进去的是 turn_id → 函数删除这一轮排队的权限请求 call_id,并清理 request_id 索引中属于这一轮的权限请求 → 出来没有返回值,权限待办集合变小。

调用关系:note_server_notification 收到 TurnCompleted 时调用它,和其他按轮次清理函数一起收尾。

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

PendingInteractiveReplayState::clear_exec_approval_turn415–426 ↗
fn clear_exec_approval_turn(&mut self, turn_id: &str)

作用:清掉某一轮对话里所有命令执行审批。因为一轮完成后,这些“是否允许执行”的旧问题已经没有继续展示的意义。

数据流:进去的是 turn_id → 函数找到这一轮保存的执行审批编号,逐个从执行审批集合删除,再从 request_id 索引里删除同一轮的执行审批记录 → 出来没有返回值。

调用关系:note_server_notification 在一轮完成时调用它;它专门处理命令执行审批这一类。

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

PendingInteractiveReplayState::clear_patch_approval_turn428–439 ↗
fn clear_patch_approval_turn(&mut self, turn_id: &str)

作用:清掉某一轮对话里所有文件修改审批。它防止已经结束的一轮里的改文件确认框在以后又被重放。

数据流:进去的是 turn_id → 函数删除这一轮的文件修改 call_id,并从 request_id 索引里去掉对应记录 → 出来没有返回值。

调用关系:note_server_notification 在 TurnCompleted 时调用它,和 clear_exec_approval_turn 等函数并排完成一轮清理。

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

PendingInteractiveReplayState::remove_call_id_from_turn_map441–449 ↗
fn remove_call_id_from_turn_map(
        call_ids_by_turn_id: &mut HashMap<String, Vec<String>>,
        call_id: &str,
    )

作用:从所有轮次的排队表里删除某个 call_id。适合只知道提示编号、不确定它属于哪一轮的时候。

数据流:进去的是“按 turn_id 存 call_id 列表”的表和要删的 call_id → 函数逐个轮次过滤掉这个编号,某轮列表空了就连这一轮也删掉 → 出来没有返回值,但传入的表被就地清理。

调用关系:note_outbound_op 和 note_server_notification 在只拿到 call_id 时会用这个小工具,避免每类提示重复写同样的清表代码。

PendingInteractiveReplayState::remove_call_id_from_turn_map_entry451–466 ↗
fn remove_call_id_from_turn_map_entry(
        call_ids_by_turn_id: &mut HashMap<String, Vec<String>>,
        turn_id: &str,
        call_id: &str,
    )

作用:从指定轮次的排队表里删除某个 call_id。比全表扫描更精确,适合已经知道 turn_id 的情况。

数据流:进去的是按轮次表、turn_id、call_id → 函数只查看这一轮,把目标编号过滤掉;如果这一轮空了,就删除整个 turn_id 条目 → 出来没有返回值,表被就地更新。

调用关系:note_outbound_op、note_evicted_server_request 和 remove_request 会用它做精确清理。

PendingInteractiveReplayState::clear468–479 ↗
fn clear(&mut self)

作用:把所有待处理互动提示一次性清空。它用于线程关闭或程序关闭这种“不需要再保留任何提示”的场景。

数据流:进去的是当前状态 → 函数清空所有集合、所有按轮次队列、所有 request_id 索引 → 出来没有返回值,状态回到空白。

调用关系:note_outbound_op 遇到 Shutdown 会调用它;note_server_notification 遇到 ThreadClosed 也会调用它。

调用图:被 2 处调用(note_outbound_op, note_server_notification)。

PendingInteractiveReplayState::remove_request481–525 ↗
fn remove_request(&mut self, request_id: &AppServerRequestId)

作用:按服务器 request_id 删除一个已解决的互动请求。它能从统一索引找到请求真实类型,再去对应清单里清理。

数据流:进去的是 request_id → 函数先从 pending_requests_by_request_id 拿出那条待办;如果没有就直接返回;如果有,就按类型删除对应集合和按轮次队列里的编号或钥匙 → 出来没有返回值,那个请求不再被认为待处理。

调用关系:note_server_notification 收到 ServerRequestResolved 通知时调用它;它会把细节清理交给 remove_call_id_from_turn_map_entry。

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

PendingInteractiveReplayState::request_matches_server_request527–560 ↗
fn request_matches_server_request(
        pending: &PendingInteractiveRequest,
        request: &ServerRequest,
    ) -> bool

作用:判断一条内部待办记录和一个服务器请求是不是同一件事。它主要用于缓存事件被挤掉时,从 request_id 索引里删掉匹配记录。

数据流:进去的是 PendingInteractiveRequest 和 ServerRequest → 函数按类型对比 turn_id、item_id、approval_id、服务器名或 request_id → 出来的是 true 或 false。

调用关系:note_evicted_server_request 在清理被淘汰请求时会用它过滤 pending_requests_by_request_id,确保删的是同一个提示。

tests::request_user_input_request592–603 ↗
fn request_user_input_request(call_id: &str, turn_id: &str) -> ServerRequest

作用:测试用的小工厂,快速造一个“工具请求用户输入”的服务器请求。这样每个测试不用重复写一大段结构体。

数据流:进去的是 call_id 和 turn_id → 函数填好固定 thread_id、空问题列表和请求编号 → 出来的是 ServerRequest::ToolRequestUserInput。

调用关系:多个快照重放测试都会调用它,用来模拟“界面正在等用户填写答案”的场景。

调用图:外部调用 2 个(Integer, new)。

tests::exec_approval_request605–629 ↗
fn exec_approval_request(
        call_id: &str,
        approval_id: Option<&str>,
        turn_id: &str,
    ) -> ServerRequest

作用:测试用来生成“请求批准执行命令”的服务器请求。它可以测试有 approval_id 和没有 approval_id 两种情况。

数据流:进去的是 call_id、可选 approval_id、turn_id → 函数填入命令、工作目录、时间等测试数据 → 出来的是 ServerRequest::CommandExecutionRequestApproval。

调用关系:执行审批相关测试调用它,验证批准后、服务器解决后、轮次完成后,快照里不会再出现旧审批。

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

tests::patch_approval_request631–643 ↗
fn patch_approval_request(call_id: &str, turn_id: &str) -> ServerRequest

作用:测试用来生成“请求批准文件修改”的服务器请求。它让文件修改审批测试更简洁。

数据流:进去的是 call_id 和 turn_id → 函数填好线程、轮次、项目编号等字段 → 出来的是 ServerRequest::FileChangeRequestApproval。

调用关系:文件修改审批测试和轮次完成测试会调用它,模拟等待用户批准改文件的情况。

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

tests::elicitation_request645–664 ↗
fn elicitation_request(server_name: &str, request_id: &str, turn_id: &str) -> ServerRequest

作用:测试用来生成一个 MCP 服务器向用户提问的请求。MCP 可以理解为外部工具服务器和应用沟通的一套协议。

数据流:进去的是 server_name、request_id、turn_id → 函数构造一个带表单 schema 的确认请求 → 出来的是 ServerRequest::McpServerElicitationRequest。

调用关系:MCP 提问被用户解决后不再重放的测试会调用它。

调用图:外部调用 2 个(String, new)。

tests::turn_completed666–680 ↗
fn turn_completed(turn_id: &str) -> ServerNotification

作用:测试用来生成“一轮对话已完成”的服务器通知。它用于触发按 turn_id 清理待办提示。

数据流:进去的是 turn_id → 函数填好 Turn 的状态、时间和空项目列表 → 出来的是 ServerNotification::TurnCompleted。

调用关系:轮次完成后丢弃待审批请求的测试会调用它。

调用图:外部调用 2 个(TurnCompleted, new)。

tests::thread_closed682–686 ↗
fn thread_closed() -> ServerNotification

作用:测试用来生成“线程关闭”的服务器通知。它用于确认关闭线程会清空所有待办提示。

数据流:进去没有参数 → 函数填入固定 thread_id → 出来的是 ServerNotification::ThreadClosed。

调用关系:线程关闭后快照不再包含待审批请求的测试会调用它。

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

tests::request_resolved688–693 ↗
fn request_resolved(request_id: AppServerRequestId) -> ServerNotification

作用:测试用来生成“某个服务器请求已解决”的通知。它模拟服务器告诉前端:这个弹窗不用再等了。

数据流:进去的是 request_id → 函数填入固定 thread_id 和这个请求编号 → 出来的是 ServerNotification::ServerRequestResolved。

调用关系:用户输入和执行审批的服务器解决场景测试会调用它。

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

tests::thread_event_snapshot_keeps_pending_request_user_input696–709 ↗
fn thread_event_snapshot_keeps_pending_request_user_input()

作用:验证还没回答的用户输入提示会保留在快照里。这样切换回来时用户还能继续看到问题。

数据流:先新建事件缓存 → 放入一个用户输入请求 → 生成快照并检查里面有一条对应请求 → 测试通过表示未解决输入不会被误删。

调用关系:它调用 ThreadEventStore::new 和 tests::request_user_input_request,覆盖 should_replay_snapshot_request 对用户输入的保留行为。

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

tests::thread_event_snapshot_drops_resolved_request_user_input_after_user_answer712–728 ↗
fn thread_event_snapshot_drops_resolved_request_user_input_after_user_answer()

作用:验证用户回答后,用户输入提示不会再出现在快照里。它防止同一个问题被重复问。

数据流:先放入一个用户输入请求 → 再记录一个 UserInputAnswer 回答 → 生成快照 → 断言快照为空。

调用关系:它通过 ThreadEventStore 的 note_outbound_op 触发 PendingInteractiveReplayState::note_outbound_op 的 FIFO 删除逻辑。

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

tests::thread_event_snapshot_drops_resolved_request_user_input_after_server_resolution731–747 ↗
fn thread_event_snapshot_drops_resolved_request_user_input_after_server_resolution()

作用:验证服务器说请求已解决后,用户输入提示也不会再重放。即使不是本地回答造成的解决,也要清理。

数据流:先放入用户输入请求 → 再放入 request_resolved 通知 → 生成快照 → 检查没有 ToolRequestUserInput 事件。

调用关系:它调用 tests::request_resolved,间接覆盖 note_server_notification 到 remove_request 的清理链路。

调用图:调用 1 个内部函数(new);外部调用 4 个(Integer, assert!, request_resolved, request_user_input_request)。

tests::thread_event_snapshot_drops_resolved_exec_approval_after_outbound_approval_id750–769 ↗
fn thread_event_snapshot_drops_resolved_exec_approval_after_outbound_approval_id()

作用:验证执行命令审批被用户批准或拒绝后,不会在快照里重放。

数据流:先放入带 approval_id 的执行审批请求 → 再记录 ExecApproval 操作 → 生成快照 → 断言快照为空。

调用关系:它使用 tests::exec_approval_request,覆盖 note_outbound_op 用 approval_id 删除执行审批的路径。

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

tests::thread_event_snapshot_drops_resolved_exec_approval_after_server_resolution772–794 ↗
fn thread_event_snapshot_drops_resolved_exec_approval_after_server_resolution()

作用:验证服务器标记执行审批请求已解决后,旧审批不会再重放。

数据流:先放入执行审批请求 → 再放入对应 request_id 的解决通知 → 生成快照 → 检查没有执行审批请求。

调用关系:它调用 tests::exec_approval_request 和 tests::request_resolved,覆盖按 request_id 删除执行审批的路径。

调用图:调用 1 个内部函数(new);外部调用 4 个(Integer, assert!, exec_approval_request, request_resolved)。

tests::thread_event_snapshot_drops_answered_request_user_input_for_multi_prompt_turn797–817 ↗
fn thread_event_snapshot_drops_answered_request_user_input_for_multi_prompt_turn()

作用:验证同一轮里先回答了一个输入问题后,新来的第二个问题仍然会保留。它检查“只删已回答的那个”,不要把整轮误清空。

数据流:先放入 call-1 → 记录一个针对 turn-1 的回答 → 再放入 call-2 → 生成快照 → 断言只剩 call-2。

调用关系:它调用 tests::request_user_input_request,专门检验 note_outbound_op 对用户输入队列的先进先出处理。

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

tests::thread_event_snapshot_keeps_newer_request_user_input_pending_when_same_turn_has_queue820–839 ↗
fn thread_event_snapshot_keeps_newer_request_user_input_pending_when_same_turn_has_queue()

作用:验证同一轮同时排着多个用户输入时,回答一次只会消掉最早的那个,后面的仍然等待。

数据流:先放入 call-1 和 call-2 → 记录一次 UserInputAnswer → 生成快照 → 断言只剩 call-2。

调用关系:它覆盖 request_user_input_call_ids_by_turn_id 这个队列设计,确保 FIFO 行为符合预期。

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

tests::thread_event_snapshot_drops_resolved_patch_approval_after_outbound_approval842–856 ↗
fn thread_event_snapshot_drops_resolved_patch_approval_after_outbound_approval()

作用:验证文件修改审批被用户处理后,不会在快照中再次出现。

数据流:先放入文件修改审批请求 → 记录 PatchApproval 操作 → 生成快照 → 断言快照为空。

调用关系:它调用 tests::patch_approval_request,覆盖 note_outbound_op 删除文件修改审批的路径。

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

tests::thread_event_snapshot_drops_pending_approvals_when_turn_completes859–877 ↗
fn thread_event_snapshot_drops_pending_approvals_when_turn_completes()

作用:验证一轮对话完成后,这一轮里还挂着的执行审批和文件修改审批都会被丢弃。

数据流:先放入执行审批和文件修改审批 → 再放入 turn_completed 通知 → 生成快照 → 检查两类审批请求都不存在。

调用关系:它调用 tests::exec_approval_request、tests::patch_approval_request 和 tests::turn_completed,覆盖 note_server_notification 的按轮次清理函数。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert!, exec_approval_request, patch_approval_request, turn_completed)。

tests::thread_event_snapshot_drops_resolved_elicitation_after_outbound_resolution880–898 ↗
fn thread_event_snapshot_drops_resolved_elicitation_after_outbound_resolution()

作用:验证 MCP 服务器提问被用户解决后,不会再重放这个提问。

数据流:先放入 MCP elicitation 请求 → 记录 ResolveElicitation 操作 → 生成快照 → 断言快照为空。

调用关系:它调用 tests::elicitation_request,覆盖 note_outbound_op 通过服务器名加 request_id 删除 MCP 提问的路径。

调用图:调用 1 个内部函数(new);外部调用 3 个(String, assert!, elicitation_request)。

tests::thread_event_store_reports_pending_thread_approvals901–918 ↗
fn thread_event_store_reports_pending_thread_approvals()

作用:验证事件缓存能正确报告“是否还有待审批事项”。

数据流:先建空缓存并确认没有审批 → 放入执行审批后确认有审批 → 发出批准操作后确认又没有审批 → 测试通过表示状态提示准确。

调用关系:它调用 tests::exec_approval_request,覆盖 PendingInteractiveReplayState::has_pending_thread_approvals 的外部可见效果。

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

tests::request_user_input_does_not_count_as_pending_thread_approval921–926 ↗
fn request_user_input_does_not_count_as_pending_thread_approval()

作用:验证用户输入问题不会被算成审批。这样界面不会把“请填答案”错误显示成“请批准”。

数据流:新建缓存 → 放入一个用户输入请求 → 调用审批状态检查 → 断言结果是 false。

调用关系:它调用 tests::request_user_input_request,保护 has_pending_thread_approvals 和 has_pending_thread_user_input 的职责边界。

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

tests::thread_event_snapshot_drops_pending_requests_when_thread_closes929–942 ↗
fn thread_event_snapshot_drops_pending_requests_when_thread_closes()

作用:验证线程关闭后,原本还没处理的请求也不会再出现在快照里。

数据流:先放入执行审批请求 → 再放入 thread_closed 通知 → 生成快照 → 检查没有执行审批请求。

调用关系:它调用 tests::exec_approval_request 和 tests::thread_closed,覆盖 note_server_notification 遇到 ThreadClosed 时调用 clear 的行为。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert!, exec_approval_request, thread_closed)。

tui/src/app/thread_routing.rs源码 ↗
orchestrationmain loop, request handling, thread switching

在这个应用里,一次会话里可能不只有一条聊天线程,比如主对话、子代理对话、评审对话等。这个文件解决的问题是:服务器不停发事件,用户又可能来回切换线程,如果没有一个统一的路由和缓存层,消息就会串线、审批请求会丢、切回旧线程时屏幕也无法恢复。它给每条线程准备一个事件通道和一个事件仓库:通道像传送带,把新事件送给当前可见的线程;仓库像收纳箱,保存线程的会话信息、历史事件、待处理请求和输入框状态。当前线程活跃时,事件直接进入界面;非当前线程收到审批或文件改动请求时,也会用醒目的方式提醒用户。它还负责把用户操作翻译成 app server 能懂的命令,比如开始一轮对话、中断、回滚、改线程名、刷新技能列表等。整体上,它是多线程聊天不乱套的关键胶水层。

函数细节60
App::shutdown_current_thread11–20 ↗
async fn shutdown_current_thread(&mut self, app_server: &mut AppServerSession)

作用:关闭当前正在看的线程订阅,让服务器不要再继续往这条线程推事件。这样切换或退出时,不会留下还在后台跑的监听任务。

数据流:它先从聊天窗口读出当前线程编号;如果有,就清掉待回滚状态,通知 app server 取消订阅,再停止本地对应的事件监听任务;如果取消订阅失败,只记录警告,不让界面崩掉。

调用关系:它把真正停止本地任务的活交给 App::abort_thread_event_listener,同时调用服务器的取消订阅接口,是线程切换或关闭时的清理动作。

调用图:调用 2 个内部函数(abort_thread_event_listener, thread_unsubscribe);外部调用 1 个(warn!)。

App::abort_thread_event_listener22–26 ↗
fn abort_thread_event_listener(&mut self, thread_id: ThreadId)

作用:停止某一条线程的后台事件监听任务。可以理解成把某条线路上的收信员叫停。

数据流:输入一个线程编号;它从保存监听任务的表里找出对应任务,找到就把任务中止,同时从表里移除;没有找到就什么也不做。

调用关系:App::shutdown_current_thread 会在退订服务器后调用它,确保本地不会继续接收这条线程的事件。

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

App::abort_all_thread_event_listeners28–36 ↗
fn abort_all_thread_event_listeners(&mut self)

作用:一次性停止所有线程的后台事件监听任务。通常用于应用退出或大规模清理。

数据流:它读取保存全部监听任务的表,把里面的任务全部取出来并逐个中止;执行后,这张任务表会被清空。

调用关系:它不依赖其他本文件函数,是全局收尾用的安全阀,防止退出时后台任务还在跑。

App::ensure_thread_channel38–42 ↗
fn ensure_thread_channel(&mut self, thread_id: ThreadId) -> &mut ThreadEventChannel

作用:确保某条线程有自己的事件通道和缓存仓库。没有就新建,有就复用。

数据流:输入线程编号;它查看线程通道表,如果这条线程还没有通道,就创建一个带固定容量的 ThreadEventChannel;最后返回这条线程的通道引用。

调用关系:很多入口都会先调用它,比如接收通知、接收请求、接收历史记录、初始化主线程、创建评审线程等,因为后续事件都必须先有地方存和传。

调用图:被 5 处调用(enqueue_primary_thread_session, enqueue_thread_history_entry_response, enqueue_thread_notification, enqueue_thread_request, try_submit_active_thread_op_via_app_server)。

App::set_thread_active44–49 ↗
async fn set_thread_active(&mut self, thread_id: ThreadId, active: bool)

作用:标记某条线程现在是不是活跃线程。活跃线程的事件会被实时送到界面。

数据流:输入线程编号和真假值;它找到对应通道,锁住事件仓库,把仓库里的 active 标记改成指定状态;如果通道不存在,就不改任何东西。

调用关系:App::activate_thread_channel 用它把线程打开,App::clear_active_thread 用它把线程关掉,是线程可见状态的基础开关。

调用图:被 2 处调用(activate_thread_channel, clear_active_thread)。

App::activate_thread_channel51–64 ↗
async fn activate_thread_channel(&mut self, thread_id: ThreadId)

作用:把一条线程设为当前活跃线程,并接管它的事件接收端。只有当前没有活跃线程时才会成功。

数据流:输入线程编号;如果已有活跃线程就直接返回;否则把该线程标记为 active,取出它保存的接收器,记录为当前活跃接收器,并刷新“其他线程有待审批”的提示。

调用关系:主线程初始化时 App::enqueue_primary_thread_session 会调用它;它内部依赖 App::set_thread_active 和 App::refresh_pending_thread_approvals。

调用图:调用 2 个内部函数(refresh_pending_thread_approvals, set_thread_active);被 1 处调用(enqueue_primary_thread_session)。

App::store_active_thread_receiver66–80 ↗
async fn store_active_thread_receiver(&mut self)

作用:在离开当前线程前,把当前线程的事件接收器和输入框状态存回仓库。这样切回来时,未读事件和正在输入的文字还在。

数据流:它读取当前活跃线程编号和聊天框里的输入状态;找到对应通道后,把当前接收器放回通道,把仓库 active 改成 false,并保存输入状态。

调用关系:这是线程切换前的“打包行李”动作,和 App::activate_thread_for_replay、App::replay_thread_snapshot 这类恢复动作配套。

App::activate_thread_for_replay82–92 ↗
async fn activate_thread_for_replay(
        &mut self,
        thread_id: ThreadId,
    ) -> Option<(mpsc::Receiver<ThreadBufferedEvent>, ThreadEventSnapshot)>

作用:准备切到某条线程并重放它的缓存内容。重放就是把之前存下来的会话、历史和事件重新画到屏幕上。

数据流:输入线程编号;它取出该线程的接收器,锁住仓库并标记 active,然后生成一个当前仓库快照;成功时返回接收器和快照,缺少通道或接收器时返回空。

调用关系:它是线程切换流程里的准备步骤,后面通常会用 App::replay_thread_snapshot 把快照内容显示出来。

App::clear_active_thread94–100 ↗
async fn clear_active_thread(&mut self)

作用:清除当前活跃线程记录。用于线程断开、关闭或切换失败时,让应用知道现在没有可实时处理的线程。

数据流:它取走当前活跃线程编号;如果有,就把这条线程标为非活跃;然后丢掉当前接收器,并刷新其他线程待审批提示。

调用关系:App::drain_active_thread_events、App::handle_thread_rollback_response 和 App::handle_active_thread_event 在发现接收器断开或切换失败时会调用它。

调用图:调用 2 个内部函数(refresh_pending_thread_approvals, set_thread_active);被 3 处调用(drain_active_thread_events, handle_active_thread_event, handle_thread_rollback_response)。

App::note_thread_outbound_op102–108 ↗
async fn note_thread_outbound_op(&mut self, thread_id: ThreadId, op: &AppCommand)

作用:记录某条线程刚刚发出了一个用户操作。这样缓存仓库能判断哪些待处理请求已经被用户回应过。

数据流:输入线程编号和操作;它找到对应线程仓库并加锁,把这个操作交给仓库记录;没有对应线程时不做事。

调用关系:App::note_active_thread_outbound_op、App::submit_thread_op 和 App::try_resolve_app_server_request 会调用它,用来维护重放和审批状态。

调用图:被 3 处调用(note_active_thread_outbound_op, submit_thread_op, try_resolve_app_server_request)。

App::note_active_thread_outbound_op110–118 ↗
async fn note_active_thread_outbound_op(&mut self, op: &AppCommand)

作用:给当前活跃线程记录一个可能影响待重放状态的操作。不是所有操作都值得记录,所以它会先筛选。

数据流:输入一个操作;它先问 ThreadEventStore 这个操作会不会改变待重放状态;如果会,再找到当前活跃线程,把记录工作交给 App::note_thread_outbound_op。

调用关系:它是当前线程快捷记录入口,内部依赖 App::note_thread_outbound_op,并用 ThreadEventStore::op_can_change_pending_replay_state 做过滤。

调用图:调用 2 个内部函数(op_can_change_pending_replay_state, note_thread_outbound_op)。

App::active_turn_id_for_thread120–124 ↗
async fn active_turn_id_for_thread(&self, thread_id: ThreadId) -> Option<String>

作用:查询某条线程当前正在进行的“轮次”编号。轮次可以理解成用户发一次问题到模型完成回答的这一段过程。

数据流:输入线程编号;它找到线程仓库并加锁,从仓库里取 active_turn_id,复制成字符串返回;没有线程或没有活跃轮次就返回空。

调用关系:App::try_submit_active_thread_op_via_app_server 在中断或追加输入时会用它,判断应该中断/引导哪一轮。

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

App::thread_label126–151 ↗
fn thread_label(&self, thread_id: ThreadId) -> String

作用:给线程生成一个适合显示给人的名字,比如“Main [default]”或带短编号的 Agent 名称。

数据流:输入线程编号;它先判断是不是主线程,准备一个兜底名字;如果导航表里有代理昵称或角色,就优先格式化成更友好的名字;如果名字太泛,就附上线程短编号。

调用关系:App::interactive_request_for_thread_request 会用它给审批请求标注来源,让用户知道是哪条线程在请求权限。

调用图:被 1 处调用(interactive_request_for_thread_request);外部调用 3 个(format!, chars, to_string)。

App::current_displayed_thread_id159–161 ↗
fn current_displayed_thread_id(&self) -> Option<ThreadId>

作用:返回用户屏幕上实际正在看的线程编号。它比单看内部 active_thread_id 更贴近用户视角。

数据流:它优先返回 active_thread_id;如果切换过程中这个值暂时没有,就退而求其次读取聊天组件当前显示的线程编号。

调用关系:App::sync_active_agent_label 用它决定页脚里应该显示哪个代理标签,避免切换瞬间显示错对象。

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

App::ignore_same_thread_resume163–176 ↗
fn ignore_same_thread_resume(
        &mut self,
        target_session: &crate::resume_picker::SessionTarget,
    ) -> bool

作用:如果用户试图恢复的会话就是当前已经打开的线程,就拦住这个重复操作并提示用户。

数据流:输入目标会话;它比较目标线程编号和当前活跃线程编号;如果相同,就在聊天窗口加一条“已经在看”的提示并返回 true,否则返回 false。

调用关系:它是恢复会话流程里的防重复检查,避免同一条线程被重复加载。

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

App::sync_active_agent_label184–190 ↗
fn sync_active_agent_label(&mut self)

作用:把当前正在看的代理名称同步到底部状态栏。多代理会话里,用户需要随时知道自己在和哪个代理线程互动。

数据流:它先用 App::current_displayed_thread_id 找到当前显示线程,再让 agent_navigation 算出合适标签,设置到聊天组件里,最后同步侧边线程界面。

调用关系:App::cache_collab_receiver_threads_for_notification 在发现协作代理活动后会调用它,让界面标签马上跟上变化。

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

App::thread_cwd192–196 ↗
async fn thread_cwd(&self, thread_id: ThreadId) -> Option<AbsolutePathBuf>

作用:查询某条线程的工作目录。工作目录就是这条线程默认读写文件的位置。

数据流:输入线程编号;它找到线程仓库并加锁,如果仓库里有会话信息,就复制其中的 cwd 返回;没有会话信息则返回空。

调用关系:App::interactive_request_for_thread_request 在构造文件改动审批时会用它,告诉用户补丁是针对哪个目录的。

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

App::thread_file_change_changes198–207 ↗
async fn thread_file_change_changes(
        &self,
        thread_id: ThreadId,
        turn_id: &str,
        item_id: &str,
    ) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>>

作用:查询某条线程某次文件改动的具体变化内容。这样审批时能展示“到底要改哪些文件”。

数据流:输入线程编号、轮次编号和项目编号;它进入线程仓库,查找对应文件变化列表并返回;找不到就返回空。

调用关系:App::interactive_request_for_thread_request 在文件修改审批里调用它,把底层变化转换成界面可读的补丁预览。

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

App::interactive_request_for_thread_request209–330 ↗
async fn interactive_request_for_thread_request(
        &self,
        thread_id: ThreadId,
        request: &ServerRequest,
    ) -> std::io::Result<Option<ThreadInteractiveRequest>>

作用:把服务器发来的请求翻译成用户能在界面上处理的互动请求,比如执行命令审批、文件补丁审批、权限审批或外部工具表单。

数据流:输入线程编号和原始 ServerRequest;它根据请求类型补充线程标签、命令、原因、工作目录、文件变化等信息;能展示给用户的就返回 ThreadInteractiveRequest,不能处理或已自动拒绝的就返回空,路径转换失败会返回错误。

调用关系:App::enqueue_thread_request 和 App::surface_pending_inactive_thread_interactive_requests 会调用它;它会向 App::thread_label、App::thread_cwd、App::thread_file_change_changes 等函数取展示所需信息。

调用图:调用 5 个内部函数(thread_cwd, thread_file_change_changes, thread_label, from_url_app_server_request, from_app_server_request);被 2 处调用(enqueue_thread_request, surface_pending_inactive_thread_interactive_requests);外部调用 3 个(AppLink, Approval, McpServerElicitation)。

App::push_thread_interactive_request332–346 ↗
fn push_thread_interactive_request(&mut self, request: ThreadInteractiveRequest)

作用:把一个已经整理好的互动请求放进聊天界面。不同类型的请求会进入不同的 UI 区域。

数据流:输入 ThreadInteractiveRequest;如果是应用链接,就打开链接视图;如果是审批,就可能先渲染补丁预览,再把审批卡片推入聊天框;如果是 MCP 服务器表单,就推入对应表单队列。

调用关系:App::enqueue_thread_request 和 App::surface_pending_inactive_thread_interactive_requests 在发现非当前线程也需要用户处理时会调用它;补丁预览交给 App::render_inactive_patch_preview。

调用图:调用 1 个内部函数(render_inactive_patch_preview);被 2 处调用(enqueue_thread_request, surface_pending_inactive_thread_interactive_requests)。

App::render_inactive_patch_preview348–363 ↗
fn render_inactive_patch_preview(&mut self, request: &ApprovalRequest)

作用:为非当前线程的文件补丁审批提前在历史里显示一段补丁预览。这样用户不用切线程也能看到大概改了什么。

数据流:输入一个审批请求;它只处理 ApplyPatch 类型,并且要求有线程标签且变化不为空;满足条件时,把变化内容做成一条历史消息插进聊天记录。

调用关系:只由 App::push_thread_interactive_request 调用,是审批展示前的辅助渲染步骤。

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

App::pending_inactive_thread_requests365–387 ↗
async fn pending_inactive_thread_requests(&self) -> Vec<(ThreadId, ServerRequest)>

作用:找出所有非当前线程里还等着重放或处理的服务器请求。

数据流:它先复制一份线程编号和仓库引用列表,避免边遍历边借用主结构;然后跳过当前活跃线程,逐个仓库取出待重放请求,汇总成“线程编号 + 请求”的列表。

调用关系:App::surface_pending_inactive_thread_interactive_requests 调用它,随后把这些请求转换成用户可见的提醒。

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

App::surface_pending_inactive_thread_interactive_requests389–406 ↗
async fn surface_pending_inactive_thread_interactive_requests(
        &mut self,
    ) -> Result<()>

作用:把非当前线程里积压的互动请求拿出来提醒用户。这样后台代理要权限时不会静悄悄卡住。

数据流:如果当前正在查看侧边父线程,它直接返回;否则取出所有非活跃线程待处理请求,逐个转换成互动请求,再推到聊天界面;转换过程出错会向上传递错误。

调用关系:它串起 App::pending_inactive_thread_requests、App::interactive_request_for_thread_request 和 App::push_thread_interactive_request,是后台请求浮到前台的流程。

调用图:调用 3 个内部函数(interactive_request_for_thread_request, pending_inactive_thread_requests, push_thread_interactive_request)。

App::submit_active_thread_op408–420 ↗
async fn submit_active_thread_op(
        &mut self,
        app_server: &mut AppServerSession,
        op: AppCommand,
    ) -> Result<()>

作用:把用户对当前线程发出的操作提交出去。比如发消息、中断、回滚等。

数据流:输入 app server 会话和操作;它先确认有活跃线程,没有就显示错误;有的话把线程编号和操作交给 App::submit_thread_op。

调用关系:它是“对当前聊天操作”的常用入口,真正的分发和服务器调用由 App::submit_thread_op 完成。

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

App::submit_thread_op422–452 ↗
async fn submit_thread_op(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        op: AppCommand,
    ) -> Result<()>

作用:把一个指定线程上的用户操作送到正确去处:要么回应待处理请求,要么发给 app server,要么提示当前 TUI 还不支持。

数据流:输入服务器连接、线程编号和操作;它先记录日志,再尝试把操作匹配成某个待审批请求的答复;如果不是答复,就尝试通过 app server 执行;成功后会记录操作、刷新待审批提示和侧边状态;都不支持时显示错误。

调用关系:App::submit_active_thread_op 会调用它;它内部把活分给 App::try_resolve_app_server_request、App::try_submit_active_thread_op_via_app_server、App::note_thread_outbound_op 等函数。

调用图:调用 7 个内部函数(op_can_change_pending_replay_state, note_thread_outbound_op, refresh_pending_thread_approvals, refresh_side_parent_status_from_store, try_resolve_app_server_request, try_submit_active_thread_op_via_app_server, log_outbound_op);被 1 处调用(submit_active_thread_op);外部调用 1 个(format!)。

App::append_message_history_entry455–471 ↗
fn append_message_history_entry(&self, thread_id: ThreadId, text: String)

作用:把用户输入的提示词追加到本机跨会话历史里。这样以后可以从历史中找回以前发过的话。

数据流:输入线程编号和文本;它根据配置构造历史记录设置,然后启动一个后台异步任务写入历史;写失败只记录警告,不阻塞当前界面。

调用关系:它使用 codex_message_history 的追加接口,是用户输入历史保存的后台小任务。

调用图:调用 1 个内部函数(new);外部调用 3 个(append_entry, spawn, warn!)。

App::lookup_message_history_entry474–505 ↗
async fn lookup_message_history_entry(
        &mut self,
        thread_id: ThreadId,
        offset: usize,
        log_id: u64,
    ) -> Result<()>

作用:按偏移量查找一条本机历史输入,并把结果发回对应线程。常用于用户按快捷键翻历史消息。

数据流:输入线程编号、偏移量和日志编号;它复制事件发送器,后台查历史;查到后把文本包成 ThreadHistoryEntryResponse 事件发回应用事件队列;函数本身很快返回。

调用关系:它通过 app_event_tx 把结果送回主事件循环,后面会进入 App::enqueue_thread_history_entry_response。

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

App::try_submit_active_thread_op_via_app_server507–735 ↗
async fn try_submit_active_thread_op_via_app_server(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        op: &AppCommand,
    ) -> Result<bool>

作用:把各种线程操作具体翻译成 app server 的接口调用。它是“用户命令到后端 API”的大分发器。

数据流:输入服务器连接、线程编号和操作引用;它按操作类型处理:中断当前轮次、开始或引导用户轮次、列技能、压缩线程、改名、回滚、启动评审、清后台终端、运行 shell、重载配置、同步上下文、批准被守护规则拦截的动作等;能处理返回 true,不能处理返回 false,服务器失败则返回错误。

调用关系:App::submit_thread_op 会调用它;它会用 App::active_turn_id_for_thread、App::ensure_thread_channel、App::handle_skills_list_result、App::handle_thread_rollback_response 等函数配合完成复杂操作。

调用图:调用 18 个内部函数(from_string, active_turn_id_for_thread, ensure_thread_channel, handle_skills_list_result, handle_thread_rollback_response, reload_user_config, review_start, skills_list, startup_interrupt, thread_approve_guardian_denied_action (+8 more));被 1 处调用(submit_thread_op);外部调用 3 个(clone, turn_permissions_override_from_config, unreachable!)。

App::turn_permissions_override_from_config737–763 ↗
fn turn_permissions_override_from_config(
        config: &Config,
        active_permission_profile: Option<&ActivePermissionProfile>,
        runtime_permission_profile_override: Option<&PermissionP

作用:决定开始新一轮对话时,要不要把本地权限设置发给服务器。权限设置决定模型能不能读写文件、访问哪些范围。

数据流:输入配置、当前活跃权限配置和运行时权限覆盖;如果已有活跃权限配置,就直接发送 ActiveProfile;如果运行时覆盖和当前有效权限一致,就用旧式沙盒设置发送;否则返回 Preserve,表示保留服务器已有快照。

调用关系:App::try_submit_active_thread_op_via_app_server 在启动 UserTurn 时调用它,保证权限不会被无意覆盖;底部测试专门验证它的三种分支。

调用图:外部调用 2 个(ActiveProfile, LegacySandbox)。

App::handle_skills_list_result765–778 ↗
fn handle_skills_list_result(
        &mut self,
        result: Result<SkillsListResponse>,
        failure_message: &str,
    )

作用:处理刷新技能列表的结果。成功就更新界面,失败就提示用户。

数据流:输入一个技能列表结果和失败提示文字;如果成功,交给 App::handle_skills_list_response;如果失败,记录警告并在聊天窗口显示错误消息。

调用关系:App::try_submit_active_thread_op_via_app_server 在执行 ListSkills 命令后调用它,是技能刷新结果的统一出口。

调用图:调用 1 个内部函数(handle_skills_list_response);被 1 处调用(try_submit_active_thread_op_via_app_server);外部调用 2 个(format!, warn!)。

App::try_resolve_app_server_request780–813 ↗
async fn try_resolve_app_server_request(
        &mut self,
        app_server: &AppServerSession,
        thread_id: ThreadId,
        op: &AppCommand,
    ) -> Result<bool>

作用:判断当前用户操作是不是在回答某个服务器请求,比如批准或拒绝权限。如果是,就把答案发回服务器。

数据流:输入服务器连接、线程编号和操作;它从 pending_app_server_requests 里尝试取出对应答复;没有答复就返回 false;有答复就调用服务器 resolve 接口,成功后更新本地待处理状态,失败则显示错误并返回 false。

调用关系:App::submit_thread_op 会先调用它,因为审批答复要优先于普通线程命令处理。

调用图:调用 5 个内部函数(op_can_change_pending_replay_state, note_thread_outbound_op, refresh_pending_thread_approvals, refresh_side_parent_status_from_store, resolve_server_request);被 1 处调用(submit_thread_op);外部调用 1 个(format!)。

App::refresh_pending_thread_approvals815–844 ↗
async fn refresh_pending_thread_approvals(&mut self)

作用:刷新界面上“哪些其他线程还有待审批”的提示列表。

数据流:它收集所有线程仓库,跳过当前活跃线程和当前侧边父线程;逐个检查是否有待审批事项;把有待审批的线程按编号排序,再转换成人类可读标签,写入聊天组件。

调用关系:线程激活、清空、接收通知、接收请求、提交操作和解决服务器请求后都会调用它,让提示保持最新。

调用图:被 6 处调用(activate_thread_channel, clear_active_thread, enqueue_thread_notification, enqueue_thread_request, submit_thread_op, try_resolve_app_server_request);外部调用 1 个(new)。

App::refresh_side_parent_status_from_store846–859 ↗
async fn refresh_side_parent_status_from_store(&mut self, thread_id: ThreadId)

作用:根据线程仓库里的状态刷新侧边父线程的动作状态。侧边父线程可以理解成某个附属视图背后的主线程。

数据流:输入线程编号;它找到仓库并读取 side_parent_pending_status;如果有状态就设置到界面,没有就清掉这条线程的侧边动作状态。

调用关系:App::submit_thread_op 和 App::try_resolve_app_server_request 在操作改变待处理状态后会调用它,确保侧边状态不滞后。

调用图:被 2 处调用(submit_thread_op, try_resolve_app_server_request)。

App::enqueue_thread_notification861–919 ↗
async fn enqueue_thread_notification(
        &mut self,
        thread_id: ThreadId,
        notification: ServerNotification,
    ) -> Result<()>

作用:把服务器通知放进对应线程的缓存和实时通道。通知是服务器主动告诉前端的事情,比如线程启动、关闭、设置更新、轮次开始等。

数据流:输入线程编号和通知;它会忽略一些不该为未知子线程创建通道的设置通知;必要时更新缓存会话,推断新会话信息,确保通道存在,把通知写入仓库;如果线程活跃,就把通知发到实时通道;最后更新侧边状态和待审批提示。

调用关系:App::enqueue_primary_thread_notification 和 App::enqueue_primary_thread_session 会调用它;它依赖 App::ensure_thread_channel、App::infer_session_for_thread_notification 和 App::refresh_pending_thread_approvals。

调用图:调用 4 个内部函数(for_notification, ensure_thread_channel, infer_session_for_thread_notification, refresh_pending_thread_approvals);被 2 处调用(enqueue_primary_thread_notification, enqueue_primary_thread_session);外部调用 6 个(clone, clone, matches!, spawn, warn!, Notification)。

App::cache_collab_receiver_threads_for_notification927–965 ↗
fn cache_collab_receiver_threads_for_notification(
        &mut self,
        notification: &ServerNotification,
    )

作用:从协作相关通知里提前记住可能出现的接收线程。这样代理选择器能尽早显示这些线程,即使完整名称稍后才拿到。

数据流:输入通知;如果通知里有可显示的子代理活动,就记录活动并同步当前代理标签;否则尝试提取接收线程编号,过滤无效或 not found 的项,把还没见过的线程加入代理选择器。

调用关系:App::handle_thread_event_now 在实时处理通知时调用它,让多代理导航信息跟着服务器事件逐步补全。

调用图:调用 2 个内部函数(from_string, sync_active_agent_label);被 1 处调用(handle_thread_event_now);外部调用 1 个(warn!)。

App::infer_session_for_thread_notification967–998 ↗
async fn infer_session_for_thread_notification(
        &mut self,
        thread_id: ThreadId,
        notification: &ServerNotification,
    ) -> Option<ThreadSessionState>

作用:从 ThreadStarted 通知里推断出一份线程会话信息。这样新线程即使还没完整恢复,也能先有基本资料。

数据流:输入线程编号和通知;只有 ThreadStarted 会被处理;它复制主会话配置,替换线程编号、名称、模型提供方、工作目录和记录路径,并尝试从本地会话库读取模型;最后更新代理选择器并返回会话状态。

调用关系:App::enqueue_thread_notification 会调用它,为新启动的线程补齐仓库里的 session 信息。

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

App::enqueue_thread_request1000–1047 ↗
async fn enqueue_thread_request(
        &mut self,
        thread_id: ThreadId,
        request: ServerRequest,
    ) -> Result<()>

作用:把服务器请求放进对应线程,并在需要时提醒用户处理。请求通常需要前端或用户给出答复。

数据流:输入线程编号和请求;如果请求来自非当前线程,先尝试转换成可显示的互动请求;然后确保通道存在,把请求写入仓库;如果线程活跃就发送到实时通道,不活跃且没有侧边父线程时就直接推到界面提醒;最后刷新侧边状态和待审批提示。

调用关系:App::enqueue_primary_thread_request 和 App::enqueue_primary_thread_session 会调用它;它串联 App::interactive_request_for_thread_request、App::push_thread_interactive_request 和 App::refresh_pending_thread_approvals。

调用图:调用 5 个内部函数(for_request, ensure_thread_channel, interactive_request_for_thread_request, push_thread_interactive_request, refresh_pending_thread_approvals);被 2 处调用(enqueue_primary_thread_request, enqueue_primary_thread_session);外部调用 5 个(clone, clone, spawn, warn!, Request)。

App::enqueue_thread_history_entry_response1049–1091 ↗
async fn enqueue_thread_history_entry_response(
        &mut self,
        thread_id: ThreadId,
        event: HistoryLookupResponse,
    ) -> Result<()>

作用:把历史输入查询结果放入某条线程。这样查到的历史消息能回到正确的聊天框。

数据流:输入线程编号和历史查询结果;它确保通道存在,把结果事件放进仓库缓冲区;如果缓冲超容量,会丢掉最旧事件,并在丢的是服务器请求时更新重放状态;线程活跃时还会把结果发到实时通道。

调用关系:App::enqueue_primary_thread_session 会在主线程建立后转交积压事件;历史查询后台任务的结果最终也会进入这个路径。

调用图:调用 1 个内部函数(ensure_thread_channel);被 1 处调用(enqueue_primary_thread_session);外部调用 5 个(clone, spawn, warn!, HistoryEntryResponse, clone)。

App::enqueue_primary_thread_session1093–1148 ↗
async fn enqueue_primary_thread_session(
        &mut self,
        session: ThreadSessionState,
        turns: Vec<Turn>,
    ) -> Result<()>

作用:登记主线程的会话信息和已有轮次,并把启动前积压的主线程事件补送进去。

数据流:输入主线程会话和历史轮次;它设置 primary_thread_id 和主会话缓存,创建通道,写入仓库,激活主线程,给聊天界面加载会话并重放历史;随后取出启动前暂存的通知、请求、历史响应和反馈事件,逐个送入正常入队流程;最后恢复初始消息自动提交。

调用关系:这是主线程真正准备好的关键入口,会调用 App::ensure_thread_channel、App::activate_thread_channel、App::enqueue_thread_notification、App::enqueue_thread_request 和 App::enqueue_thread_history_entry_response。

调用图:调用 5 个内部函数(activate_thread_channel, enqueue_thread_history_entry_response, enqueue_thread_notification, enqueue_thread_request, ensure_thread_channel);外部调用 2 个(take, clone)。

App::enqueue_primary_thread_notification1150–1162 ↗
async fn enqueue_primary_thread_notification(
        &mut self,
        notification: ServerNotification,
    ) -> Result<()>

作用:接收发给主线程的服务器通知。如果主线程还没建立,就先暂存。

数据流:输入通知;如果已有 primary_thread_id,就转交 App::enqueue_thread_notification;否则把通知包成 ThreadBufferedEvent 放进 pending_primary_events。

调用关系:它解决启动早期的竞态:服务器可能先发通知,而主线程会话稍后才配置完成。

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

App::enqueue_primary_thread_request1164–1174 ↗
async fn enqueue_primary_thread_request(
        &mut self,
        request: ServerRequest,
    ) -> Result<()>

作用:接收发给主线程的服务器请求。如果主线程还没建立,就先暂存。

数据流:输入请求;如果主线程编号已知,就转交 App::enqueue_thread_request;否则把请求包成 ThreadBufferedEvent 存进 pending_primary_events。

调用关系:它和 App::enqueue_primary_thread_notification 一样,是主线程初始化前的临时收件箱。

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

App::refresh_snapshot_session_if_needed1176–1203 ↗
async fn refresh_snapshot_session_if_needed(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        is_replay_only: bool,
        snapshot: &mut ThreadEvent

作用:在线程重放前,如果快照里的会话信息不完整,就尝试向服务器刷新一份。

数据流:输入服务器连接、线程编号、是否只重放标记和快照;它先用 App::should_refresh_snapshot_session 判断是否需要刷新;需要时调用 resume_thread,成功就把结果应用到快照和仓库,失败只记录警告。

调用关系:它把判断交给 App::should_refresh_snapshot_session,把成功后的写回交给 App::apply_refreshed_snapshot_thread,是切换线程前的补资料步骤。

调用图:调用 3 个内部函数(apply_refreshed_snapshot_thread, should_refresh_snapshot_session, resume_thread);外部调用 1 个(warn!)。

App::should_refresh_snapshot_session1205–1216 ↗
fn should_refresh_snapshot_session(
        &self,
        thread_id: ThreadId,
        is_replay_only: bool,
        snapshot: &ThreadEventSnapshot,
    ) -> bool

作用:判断某个线程快照是否需要重新从服务器补会话信息。

数据流:输入线程编号、是否只重放、快照;如果只是重放,或这是侧边线程,就不刷新;如果快照没有 session,或 session 里的模型为空、记录路径缺失,就返回 true。

调用关系:只由 App::refresh_snapshot_session_if_needed 调用,用来避免不必要的服务器请求。

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

App::apply_refreshed_snapshot_thread1218–1235 ↗
async fn apply_refreshed_snapshot_thread(
        &mut self,
        thread_id: ThreadId,
        started: AppServerStartedThread,
        snapshot: &mut ThreadEventSnapshot,
    )

作用:把服务器刚刷新到的线程会话和历史轮次写回本地快照和仓库。

数据流:输入线程编号、服务器返回的 started 数据和可变快照;它更新对应仓库的 session 和 turns,并重整缓冲区;然后把 session、turns 写进快照,并移除刷新后不该保留的旧事件。

调用关系:App::refresh_snapshot_session_if_needed 在 resume_thread 成功后调用它,保证接下来的重放用的是更新后的资料。

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

App::drain_active_thread_events1243–1270 ↗
async fn drain_active_thread_events(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:把当前活跃线程通道里已经到达的事件一次性取出来处理。它是主循环里“清空收件箱”的动作。

数据流:它取出当前活跃接收器;不断尝试读取事件,读到就交给 App::handle_thread_event_now;读空就停止;如果通道断开,就清除活跃线程;如果回滚渲染待处理,就请求界面重画。

调用关系:它调用 App::handle_thread_event_now 处理普通实时事件,必要时调用 App::clear_active_thread 处理断线。

调用图:调用 2 个内部函数(clear_active_thread, handle_thread_event_now);外部调用 1 个(frame_requester)。

App::active_non_primary_shutdown_target1283–1296 ↗
fn active_non_primary_shutdown_target(
        &self,
        notification: &ServerNotification,
    ) -> Option<(ThreadId, ThreadId)>

作用:判断当前事件是不是“正在看的非主线程意外关闭”,如果是,就告诉调用者应该切回主线程。

数据流:输入服务器通知;只有 ThreadClosed 才继续;它读取当前活跃线程、主线程和待退出线程标记;如果关闭的是用户主动退出的线程就忽略;如果活跃线程不是主线程,就返回关闭线程和主线程编号。

调用关系:App::handle_active_thread_event 用它在处理关闭事件前先做故障转移判断。

调用图:被 1 处调用(handle_active_thread_event);外部调用 1 个(matches!)。

App::replay_thread_snapshot1298–1347 ↗
fn replay_thread_snapshot(
        &mut self,
        snapshot: ThreadEventSnapshot,
        resume_restored_queue: bool,
    )

作用:把某条线程的快照重新播放到聊天界面。用于用户切换回旧线程时恢复屏幕内容。

数据流:输入线程快照和是否恢复队列标记;它先发送重放开始信号,加载 session,恢复输入框状态,重放历史轮次,再逐个重放缓存事件;如果有待审批请求,会压掉一些重复通知;最后恢复自动发送设置、提交待发初始消息,并刷新状态栏。

调用关系:它会调用 App::handle_thread_event_replay 来处理每个缓存事件,是线程切换显示的核心步骤。

调用图:调用 3 个内部函数(event_is_notice, snapshot_has_pending_interactive_request, handle_thread_event_replay)。

App::should_wait_for_initial_session1349–1354 ↗
fn should_wait_for_initial_session(session_selection: &SessionSelection) -> bool

作用:判断启动时是否应该等待初始主线程会话配置完成。

数据流:输入会话选择;如果用户选择新开会话或退出,就返回 true;其他情况返回 false。

调用关系:这是启动流程里的小判断函数,帮助主循环决定是否先暂停处理活跃线程事件。

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

App::should_prompt_for_paused_goal_after_startup_resume1356–1364 ↗
fn should_prompt_for_paused_goal_after_startup_resume(
        session_selection: &SessionSelection,
        initial_prompt: &Option<String>,
        initial_images: &[PathBuf],
    ) -> bool

作用:判断恢复会话后是否要提示用户继续之前暂停的目标。

数据流:输入会话选择、初始文字提示和初始图片列表;只有在恢复旧会话、没有初始文字、也没有初始图片时返回 true。

调用关系:它服务于启动恢复体验,避免用户已经带了新输入时还弹出不合时宜的继续提示。

调用图:外部调用 2 个(is_empty, matches!)。

App::should_handle_active_thread_events1366–1371 ↗
fn should_handle_active_thread_events(
        waiting_for_initial_session_configured: bool,
        has_active_thread_receiver: bool,
    ) -> bool

作用:判断现在能不能处理活跃线程事件。

数据流:输入是否还在等初始会话、是否有活跃线程接收器;只有接收器存在且不再等待初始会话时返回 true。

调用关系:这是主循环的门卫条件,防止启动早期过早消费线程事件。

App::should_stop_waiting_for_initial_session1373–1378 ↗
fn should_stop_waiting_for_initial_session(
        waiting_for_initial_session_configured: bool,
        primary_thread_id: Option<ThreadId>,
    ) -> bool

作用:判断是否可以结束“等待初始会话”的状态。

数据流:输入当前是否在等待、主线程编号是否存在;只有正在等待且主线程编号已经有值时返回 true。

调用关系:它和 App::should_wait_for_initial_session、App::should_handle_active_thread_events 配合,控制启动阶段的事件处理节奏。

App::handle_skills_list_response1381–1387 ↗
fn handle_skills_list_response(&mut self, response: SkillsListResponse)

作用:处理成功拿到的技能列表,并发出新出现的技能加载警告。

数据流:输入技能列表响应;它读取当前工作目录,找出和该目录相关的错误,只保留新近变为活跃的错误,发出警告事件,然后把完整响应交给聊天组件显示。

调用关系:App::handle_skills_list_result 在技能列表请求成功时调用它。

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

App::handle_thread_rollback_response1389–1421 ↗
async fn handle_thread_rollback_response(
        &mut self,
        thread_id: ThreadId,
        num_turns: u32,
        response: &ThreadRollbackResponse,
    )

作用:处理线程回滚成功后的本地状态清理。回滚就是把对话倒回前几轮。

数据流:输入线程编号、回滚轮数和服务器响应;它把响应应用到对应线程仓库;如果回滚的是当前活跃线程,还会清空接收器里已经排队但过时的事件;最后通知回滚 UI 成功。

调用关系:App::try_submit_active_thread_op_via_app_server 在 thread_rollback 成功后调用它;如果接收器断开,会调用 App::clear_active_thread。

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

App::handle_thread_event_now1423–1454 ↗
fn handle_thread_event_now(&mut self, event: ThreadBufferedEvent)

作用:立即处理当前活跃线程的新事件,把它反映到聊天界面。

数据流:输入一个 ThreadBufferedEvent;通知会先更新协作线程缓存,再交给聊天组件;请求只有还在待处理表里才展示;历史响应交给历史处理;反馈事件交给反馈处理;遇到影响状态栏的事件会刷新状态栏。

调用关系:App::drain_active_thread_events 和 App::handle_active_thread_event 都会调用它;它内部会用 App::cache_collab_receiver_threads_for_notification 更新多代理导航。

调用图:调用 1 个内部函数(cache_collab_receiver_threads_for_notification);被 2 处调用(drain_active_thread_events, handle_active_thread_event);外部调用 1 个(matches!)。

App::handle_thread_event_replay1456–1471 ↗
fn handle_thread_event_replay(&mut self, event: ThreadBufferedEvent)

作用:处理从快照里重放出来的旧事件。和实时处理类似,但会告诉聊天组件这是重放,不是新事件。

数据流:输入缓存事件;通知和请求会带着 ThreadSnapshot 重放标记交给聊天组件;历史响应和反馈事件按正常方式处理;它不会做实时专用的过滤和状态栏刷新。

调用关系:App::replay_thread_snapshot 会对快照里的每个事件调用它。

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

App::handle_active_thread_event1478–1539 ↗
async fn handle_active_thread_event(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        event: ThreadBufferedEvent,
    ) -> Result<()>

作用:处理当前活跃线程弹出的单个事件,并特别处理线程关闭这种容易造成混乱的情况。

数据流:输入界面对象、服务器连接和事件;它先判断这个事件是否完成了用户主动关闭线程;如果是非主线程意外关闭,就标记该线程关闭并尝试切回主线程;如果是用户主动关闭,则清掉退出标记;普通事件交给 App::handle_thread_event_now;需要时请求界面重画。

调用关系:它依赖 App::active_non_primary_shutdown_target 判断是否要故障转移,也可能调用 App::clear_active_thread;正常事件处理则交给 App::handle_thread_event_now。

调用图:调用 3 个内部函数(active_non_primary_shutdown_target, clear_active_thread, handle_thread_event_now);外部调用 3 个(frame_requester, format!, matches!)。

tests::config_with_workspace_profile1548–1559 ↗
async fn config_with_workspace_profile() -> Config

作用:为测试创建一份临时配置,默认使用 workspace 权限配置。这样测试不会污染真实用户目录。

数据流:它创建临时目录,把它作为 codex_home,并设置默认权限覆盖,然后异步构建 Config 返回。

调用关系:下面三个权限相关测试都会调用它,作为共同的测试夹具。

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

tests::turn_permissions_use_active_profile_when_available1562–1576 ↗
async fn turn_permissions_use_active_profile_when_available()

作用:测试当配置里有活跃权限档案时,启动轮次会优先发送 ActiveProfile。

数据流:它先创建测试配置,取出 active_permission_profile,然后调用 App::turn_permissions_override_from_config,并断言结果等于 workspace 的 ActiveProfile。

调用关系:它验证 App::turn_permissions_override_from_config 的第一条优先级规则。

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

tests::turn_permissions_preserve_server_snapshot_without_local_override1579–1593 ↗
async fn turn_permissions_preserve_server_snapshot_without_local_override()

作用:测试没有本地运行时覆盖时,不应随便覆盖服务器保存的权限快照。

数据流:它创建配置后把权限改成只读,再调用 App::turn_permissions_override_from_config,传入没有活跃档案、没有运行时覆盖,最后断言结果是 Preserve。

调用关系:它验证 App::turn_permissions_override_from_config 在普通本地配置变化下会保守处理。

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

tests::turn_permissions_send_legacy_sandbox_for_local_override1596–1613 ↗
async fn turn_permissions_send_legacy_sandbox_for_local_override()

作用:测试当用户确实在运行时设置了权限覆盖时,会按旧式沙盒参数发送给服务器。

数据流:它创建配置,把权限设为 workspace write,取出有效权限,再把同一个权限作为运行时覆盖传给 App::turn_permissions_override_from_config,断言结果是 LegacySandbox。

调用关系:它验证 App::turn_permissions_override_from_config 对运行时覆盖的兼容分支。

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

后台副作用

此文件将网络和磁盘密集型工作卸载到生成的任务中,并通过应用事件返回结果。

tui/src/app/background_requests.rs源码 ↗
orchestrationcross-cutting

终端界面需要经常问后台服务要数据,或者把用户的设置写回去。如果这些事都在主界面线程里做,界面就会像点了按钮后整个人“冻住”一样。这个文件的做法是:App 收到需要联网或写配置的动作后,先拿到 app-server 的请求句柄,再用 tokio::spawn(启动一个后台异步任务)去做真正请求;请求结束后,不直接改界面,而是发一个 AppEvent 事件回主循环。这样界面更新仍然集中在一个地方,比较安全,也不容易乱。它还处理一些细节:给某些请求加超时,避免一直等;把插件远程目录的错误翻译成用户能看懂的下一步建议;防止插件/Hook 开关被用户连点时写配置打架;反馈提交完成后按原会话线程显示结果。

函数细节69
App::fetch_mcp_inventory36–55 ↗
fn fetch_mcp_inventory(
        &mut self,
        app_server: &AppServerSession,
        detail: McpServerStatusDetail,
        thread_id: Option<ThreadId>,
    )

作用:在后台读取 MCP 服务器清单,也就是当前可用的外部工具、资源和登录状态。它的价值是让用户能看到工具列表,同时不阻塞聊天界面。

数据流:输入是 app-server 会话、想看的详细程度、可选的线程编号。它先判断这个线程编号是否还适合带给服务器,然后启动后台任务去调用 fetch_all_mcp_server_statuses;完成后把成功清单或错误文字包装成 AppEvent::McpInventoryLoaded 发回主界面。

调用关系:这是用户或界面需要展示 MCP 清单时的入口。它先问 App::mcp_inventory_request_thread_id 要不要带线程信息,再把真正取数据的活交给 fetch_all_mcp_server_statuses。

调用图:调用 3 个内部函数(mcp_inventory_request_thread_id, fetch_all_mcp_server_statuses, request_handle);外部调用 1 个(spawn)。

App::mcp_inventory_request_thread_id57–65 ↗
fn mcp_inventory_request_thread_id(&self, thread_id: Option<ThreadId>) -> Option<ThreadId>

作用:判断读取 MCP 清单时,是否应该把当前线程编号发给后台。它避免给已经关闭或不再活跃的代理线程发送过时上下文。

数据流:输入是一个可选线程编号。它读取 App 当前活跃线程和代理导航状态;如果线程仍是当前线程且没有关闭,就原样返回,否则返回 None。

调用关系:只被 App::fetch_mcp_inventory 调用。它像一个门卫,先确认线程还有效,再允许请求带着这个线程身份出门。

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

App::refresh_rate_limits74–97 ↗
fn refresh_rate_limits(
        &mut self,
        app_server: &AppServerSession,
        origin: RateLimitRefreshOrigin,
    )

作用:在后台刷新账号的使用额度和限流信息。用户打开状态页、启动预取或消耗重置额度后,都需要这个信息。

数据流:输入是 app-server 会话和刷新来源。它启动后台请求 fetch_account_rate_limits;如果来源是重置额度相关操作,就加 15 秒超时;最后把结果和来源一起发成 AppEvent::RateLimitsLoaded。

调用关系:它是界面触发额度刷新时的调度点。真正跟服务器说话的是 fetch_account_rate_limits,结果交回主事件循环处理。

调用图:调用 2 个内部函数(fetch_account_rate_limits, request_handle);外部调用 2 个(spawn, timeout)。

App::refresh_token_activity99–116 ↗
fn refresh_token_activity(
        &mut self,
        app_server: &AppServerSession,
        request_id: u64,
    )

作用:读取账号的 token 使用活动。token 可以粗略理解为模型处理文字时消耗的计量单位。

数据流:输入是 app-server 会话和请求编号。它在后台调用 fetch_account_token_activity,并设置 15 秒超时;完成后把请求编号和结果发回 AppEvent::TokenActivityLoaded。

调用关系:通常由状态或用量界面发起。它只负责开后台任务,实际读取由 fetch_account_token_activity 完成。

调用图:调用 2 个内部函数(fetch_account_token_activity, request_handle);外部调用 2 个(spawn, timeout)。

App::refresh_rate_limit_reset_credits118–135 ↗
fn refresh_rate_limit_reset_credits(
        &mut self,
        app_server: &AppServerSession,
        request_id: u64,
    )

作用:在后台读取“限流重置额度”的剩余情况。这个额度可以用来解除或缓解某些速率限制。

数据流:输入是 app-server 会话和请求编号。它调用 fetch_rate_limit_reset_credits,并加 15 秒超时;成功得到额度摘要,失败得到错误文字,然后发 AppEvent::RateLimitResetCreditsLoaded。

调用关系:这是界面查询重置额度的入口。它把网络请求交给 fetch_rate_limit_reset_credits,自己负责超时和事件回传。

调用图:调用 2 个内部函数(fetch_rate_limit_reset_credits, request_handle);外部调用 2 个(spawn, timeout)。

App::consume_rate_limit_reset_credit137–159 ↗
fn consume_rate_limit_reset_credit(
        &mut self,
        app_server: &AppServerSession,
        request_id: u64,
        idempotency_key: String,
    )

作用:在后台使用一次限流重置额度。幂等键用于防止同一次操作被重复扣费或重复执行。

数据流:输入是 app-server 会话、请求编号和幂等键。它启动后台任务调用 consume_rate_limit_reset_credit_request,最多等 15 秒;结束后把请求编号、幂等键和结果发回 AppEvent::RateLimitResetCreditConsumed。

调用关系:当用户确认使用重置额度时会走这里。真正提交消耗请求的是 consume_rate_limit_reset_credit_request。

调用图:调用 2 个内部函数(consume_rate_limit_reset_credit_request, request_handle);外部调用 2 个(spawn, timeout)。

App::send_add_credits_nudge_email161–174 ↗
fn send_add_credits_nudge_email(
        &mut self,
        app_server: &AppServerSession,
        credit_type: AddCreditsNudgeCreditType,
    )

作用:请求后台发送一封“增加额度提醒”邮件。它用于用户额度不足时,引导用户或管理员处理额度问题。

数据流:输入是 app-server 会话和额度类型。它后台调用同名的自由函数 send_add_credits_nudge_email;完成后把成功状态或错误发成 AppEvent::AddCreditsNudgeEmailFinished。

调用关系:界面上触发发送提醒邮件时调用它。它只是外层调度,真正 RPC 请求由 send_add_credits_nudge_email 完成。

调用图:调用 2 个内部函数(send_add_credits_nudge_email, request_handle);外部调用 1 个(spawn)。

App::refresh_startup_skills183–193 ↗
fn refresh_startup_skills(&mut self, app_server: &AppServerSession)

作用:应用启动时在后台加载技能列表。这样第一屏可以先显示出来,不必等技能元数据全部取完。

数据流:输入是 app-server 会话。它读取当前工作目录,后台调用 fetch_skills_list;成功拿到技能列表,失败拿到格式化错误,然后发 AppEvent::SkillsListLoaded。

调用关系:启动流程会调用它。它把慢的技能扫描放到后台,结果仍走统一事件队列,让后续界面更新保持一致。

调用图:调用 2 个内部函数(fetch_skills_list, request_handle);外部调用 1 个(spawn)。

App::fetch_connectors_list195–214 ↗
fn fetch_connectors_list(
        &mut self,
        app_server: &AppServerSession,
        force_refetch: bool,
    )

作用:在后台读取连接器列表,也就是可连接的外部应用或服务。force_refetch 表示是否强制重新向后台取最新数据。

数据流:输入是 app-server 会话和是否强制刷新。它读取当前显示线程编号,后台调用 fetch_connectors_list;完成后发 AppEvent::ConnectorsLoaded,并标记这是最终结果。

调用关系:连接器界面需要刷新时调用它。它把请求交给同名自由函数 fetch_connectors_list,自己负责带上当前线程上下文和回传事件。

调用图:调用 2 个内部函数(fetch_connectors_list, request_handle);外部调用 1 个(spawn)。

App::fetch_plugins_list216–246 ↗
fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf)

作用:在后台读取插件列表,并在成功后继续读取远程插件区块。它让插件菜单可以先显示基础内容,再补上远程市场、共享插件等内容。

数据流:输入是 app-server 会话和当前目录。它先通知聊天组件插件列表开始加载,然后后台调用 fetch_plugins_list;如果基础列表成功,再调用 fetch_additional_plugin_remote_sections 读取额外远程区块;两个阶段分别发 PluginsLoaded 和 PluginRemoteSectionsLoaded。

调用关系:插件菜单打开或刷新时会用它。它串起基础插件列表和额外远程目录两个请求。

调用图:调用 3 个内部函数(fetch_additional_plugin_remote_sections, fetch_plugins_list, request_handle);外部调用 2 个(clone, spawn)。

App::fetch_hooks_list248–257 ↗
fn fetch_hooks_list(&mut self, app_server: &AppServerSession, cwd: PathBuf)

作用:在后台读取 Hook 列表。Hook 可以理解为某些事件发生时自动执行的小动作或脚本。

数据流:输入是 app-server 会话和当前目录。它后台调用 hooks_rpc 里的 fetch_hooks_list,拿到列表或错误后发 AppEvent::HooksLoaded。

调用关系:Hook 设置界面需要展示当前 Hook 时调用它。实际读取逻辑在 hooks_rpc 模块里。

调用图:调用 2 个内部函数(request_handle, fetch_hooks_list);外部调用 2 个(clone, spawn)。

App::fetch_plugin_detail259–273 ↗
fn fetch_plugin_detail(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        params: PluginReadParams,
    )

作用:读取某个插件的详细信息,比如说明、配置或来源。用户点开插件详情时需要它。

数据流:输入是 app-server 会话、当前目录和读取参数。它后台调用 fetch_plugin_detail,完成后把目录和结果发成 AppEvent::PluginDetailLoaded。

调用关系:插件详情页触发它。真正的服务器请求由同名自由函数 fetch_plugin_detail 做。

调用图:调用 2 个内部函数(fetch_plugin_detail, request_handle);外部调用 1 个(spawn)。

App::fetch_marketplace_add275–295 ↗
fn fetch_marketplace_add(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        source: String,
    )

作用:在后台添加一个插件市场来源。插件市场可以理解为插件的仓库或目录。

数据流:输入是 app-server 会话、当前目录和市场来源字符串。它保留一份目录和来源用于回显,然后后台调用 fetch_marketplace_add;结果会带上用户看得懂的失败前缀,再发 MarketplaceAddLoaded。

调用关系:用户添加插件市场时调用它。它把具体请求交给 fetch_marketplace_add。

调用图:调用 2 个内部函数(fetch_marketplace_add, request_handle);外部调用 2 个(clone, spawn)。

App::fetch_marketplace_remove297–319 ↗
fn fetch_marketplace_remove(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        marketplace_name: String,
        marketplace_display_name: String,
    )

作用:在后台移除一个插件市场。它既保留内部名字,也保留显示名,方便结果回来后界面提示用户。

数据流:输入是 app-server 会话、当前目录、市场内部名和显示名。它后台调用 fetch_marketplace_remove;完成后发 MarketplaceRemoveLoaded,里面带着目录、市场名、显示名和结果。

调用关系:用户删除插件市场时调用。真正删除请求由 fetch_marketplace_remove 发送给 app-server。

调用图:调用 2 个内部函数(fetch_marketplace_remove, request_handle);外部调用 2 个(clone, spawn)。

App::fetch_marketplace_upgrade321–339 ↗
fn fetch_marketplace_upgrade(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        marketplace_name: Option<String>,
    )

作用:在后台升级插件市场,可以升级指定市场,也可以让后台决定升级哪些。它用于更新插件目录内容。

数据流:输入是 app-server 会话、当前目录和可选市场名。它后台调用 fetch_marketplace_upgrade,完成后把目录和结果发 AppEvent::MarketplaceUpgradeLoaded。

调用关系:插件市场刷新或升级按钮会触发它。底层 RPC 由 fetch_marketplace_upgrade 完成。

调用图:调用 2 个内部函数(fetch_marketplace_upgrade, request_handle);外部调用 2 个(clone, spawn)。

App::fetch_plugin_install341–366 ↗
fn fetch_plugin_install(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        location: PluginLocation,
        plugin_name: String,
        plugin_display_name: Str

作用:在后台安装一个插件。它保留插件的内部名和显示名,这样结果回来时能准确告诉用户装的是哪个。

数据流:输入是 app-server 会话、当前目录、插件所在位置、插件名和显示名。它后台调用 fetch_plugin_install;完成后发 PluginInstallLoaded,带上安装位置、名称和结果。

调用关系:用户点击安装插件时调用。它把安装请求交给 fetch_plugin_install,界面更新交给事件处理器。

调用图:调用 2 个内部函数(fetch_plugin_install, request_handle);外部调用 3 个(clone, spawn, clone)。

App::fetch_plugin_uninstall368–390 ↗
fn fetch_plugin_uninstall(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        plugin_id: String,
        plugin_display_name: String,
    )

作用:在后台卸载一个插件。它让卸载过程不挡住界面操作。

数据流:输入是 app-server 会话、当前目录、插件 ID 和显示名。它后台调用 fetch_plugin_uninstall;完成后发 PluginUninstallLoaded。

调用关系:用户点击卸载插件时调用。真正发送卸载命令的是 fetch_plugin_uninstall。

调用图:调用 2 个内部函数(fetch_plugin_uninstall, request_handle);外部调用 2 个(clone, spawn)。

App::set_plugin_enabled392–407 ↗
fn set_plugin_enabled(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        plugin_id: String,
        enabled: bool,
    )

作用:处理用户打开或关闭插件的动作。它会合并连续点击,避免同一个插件的配置写入同时跑好几次。

数据流:输入是 app-server 会话、目录、插件 ID 和目标开关状态。它先检查这个插件是否已有正在写入的任务;如果有,就只记录最后一次想要的状态;如果没有,就登记为正在写入并调用 spawn_plugin_enabled_write。

调用关系:插件开关被用户切换时进入这里。它是防抖和排队层,实际写配置由 App::spawn_plugin_enabled_write 完成。

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

App::spawn_plugin_enabled_write409–432 ↗
fn spawn_plugin_enabled_write(
        &mut self,
        app_server: &AppServerSession,
        cwd: PathBuf,
        plugin_id: String,
        enabled: bool,
    )

作用:真正启动后台任务,把某个插件的启用状态写进配置。它把写配置这种慢操作从界面里拿出去。

数据流:输入是 app-server 会话、目录、插件 ID 和开关状态。它后台调用 write_plugin_enabled;完成后发 PluginEnabledSet,包含写入结果和这次写入的目标状态。

调用关系:由 App::set_plugin_enabled 调用。它把写入请求交给 write_plugin_enabled,之后主事件循环会根据事件处理成功、失败或排队的下一次写入。

调用图:调用 2 个内部函数(write_plugin_enabled, request_handle);被 1 处调用(set_plugin_enabled);外部调用 2 个(clone, spawn)。

App::set_hook_enabled434–447 ↗
fn set_hook_enabled(
        &mut self,
        app_server: &AppServerSession,
        key: String,
        enabled: bool,
    )

作用:处理用户打开或关闭 Hook 的动作。它和插件开关类似,会把连续改动合并,防止配置写入互相踩踏。

数据流:输入是 app-server 会话、Hook 的 key 和目标状态。它检查是否已有这个 Hook 的写入在路上;有就记录最新目标状态,没有就登记并调用 spawn_hook_enabled_write。

调用关系:Hook 设置开关变化时调用。它负责排队策略,实际写入由 App::spawn_hook_enabled_write 完成。

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

App::spawn_hook_enabled_write449–474 ↗
fn spawn_hook_enabled_write(
        &mut self,
        app_server: &AppServerSession,
        key: String,
        enabled: bool,
    )

作用:启动后台任务,把某个 Hook 的启用状态写入配置文件。失败时会把配置错误整理成更好读的文字。

数据流:输入是 app-server 会话、Hook key 和开关状态。它调用 write_hook_enabled;成功变成空结果,失败格式化为错误消息,然后发 HookEnabledSet。

调用关系:由 App::set_hook_enabled 调用。底层配置写入由 write_hook_enabled 完成。

调用图:调用 2 个内部函数(write_hook_enabled, request_handle);被 1 处调用(set_hook_enabled);外部调用 1 个(spawn)。

App::trust_hook476–491 ↗
fn trust_hook(
        &mut self,
        app_server: &AppServerSession,
        key: String,
        current_hash: String,
    )

作用:把单个 Hook 标记为可信。current_hash 用来确认用户信任的是当前这份 Hook 内容,而不是后来被改过的内容。

数据流:输入是 app-server 会话、Hook key 和当前内容哈希。它后台调用 write_hook_trust;成功后发 HookTrusted,失败则带上格式化后的错误。

调用关系:用户确认信任某个 Hook 时调用。实际信任写入在 hooks_rpc 的 write_hook_trust 中。

调用图:调用 2 个内部函数(request_handle, write_hook_trust);外部调用 1 个(spawn)。

App::trust_hooks493–507 ↗
fn trust_hooks(
        &mut self,
        app_server: &AppServerSession,
        updates: Vec<HookTrustUpdate>,
    )

作用:一次性信任多个 Hook。它适合批量确认,避免用户一个个点。

数据流:输入是 app-server 会话和多条信任更新。它后台调用 write_hook_trusts;完成后统一发 HookTrusted,里面只有成功或失败。

调用关系:批量信任操作会调用它。实际写入由 hooks_rpc 的 write_hook_trusts 完成。

调用图:调用 2 个内部函数(request_handle, write_hook_trusts);外部调用 1 个(spawn)。

App::refresh_plugin_mentions509–530 ↗
fn refresh_plugin_mentions(&mut self, app_server: &AppServerSession)

作用:刷新聊天输入里可被提及的插件候选项。这样用户输入插件提及时能看到自动补全。

数据流:它读取当前目录和功能开关。如果插件功能没开,直接发 plugins: None;如果开了,就后台调用 fetch_plugin_mentions,成功后发 PluginMentionsLoaded,失败只写日志警告。

调用关系:输入框或插件状态变化时会触发它。它依赖 plugin_mentions 模块实际拉取候选插件。

调用图:调用 2 个内部函数(fetch_plugin_mentions, request_handle);外部调用 2 个(spawn, warn!)。

App::submit_feedback532–568 ↗
fn submit_feedback(
        &mut self,
        app_server: &AppServerSession,
        category: FeedbackCategory,
        reason: Option<String>,
        turn_id: Option<String>,
        include_logs:

作用:提交用户反馈,可以选择附带日志。它把反馈上传放到后台,避免提交时卡住聊天界面。

数据流:输入是反馈类别、原因、可选 turn_id、是否带日志。它读取当前线程和日志路径,调用 build_feedback_upload_params 组装上传参数,再后台调用 fetch_feedback_upload;完成后发 FeedbackSubmitted。

调用关系:用户在反馈面板提交时调用。参数组装由 build_feedback_upload_params 做,实际上传由 fetch_feedback_upload 做。

调用图:调用 3 个内部函数(build_feedback_upload_params, fetch_feedback_upload, request_handle);外部调用 1 个(spawn)。

App::handle_feedback_thread_event570–587 ↗
fn handle_feedback_thread_event(&mut self, event: FeedbackThreadEvent)

作用:把反馈提交结果显示到聊天历史里。成功显示感谢和反馈编号,失败显示错误。

数据流:输入是一个 FeedbackThreadEvent。它看 result 是成功还是失败;成功就生成反馈成功的历史单元,失败就生成错误历史单元,并加入聊天历史。

调用关系:由 App::handle_feedback_submitted 在没有原始线程或需要直接显示时调用。它不上传,只负责把结果变成用户能看到的消息。

调用图:被 1 处调用(handle_feedback_submitted);外部调用 3 个(feedback_success_cell, format!, new_error_event)。

App::enqueue_thread_feedback_event589–630 ↗
async fn enqueue_thread_feedback_event(
        &mut self,
        thread_id: ThreadId,
        event: FeedbackThreadEvent,
    )

作用:把反馈结果投递到对应聊天线程的事件队列。这样即使用户切到别的线程,反馈结果也能回到原来的线程里。

数据流:输入是线程编号和反馈事件。它确保线程通道存在,把事件放进缓冲区;如果缓冲满了,会丢掉最旧内容并记录必要信息;如果线程当前活跃,就尝试立刻发送,通道满时再开后台任务慢慢送。

调用关系:由 App::handle_feedback_submitted 调用。它负责跨线程投递反馈结果,失败时只写日志警告。

调用图:被 1 处调用(handle_feedback_submitted);外部调用 5 个(clone, spawn, warn!, clone, FeedbackSubmission)。

App::handle_feedback_submitted632–650 ↗
async fn handle_feedback_submitted(
        &mut self,
        origin_thread_id: Option<ThreadId>,
        category: FeedbackCategory,
        include_logs: bool,
        result: Result<String, String

作用:处理反馈上传完成后的统一入口。它决定结果应该直接显示,还是送回某个原始聊天线程。

数据流:输入是原线程编号、反馈类别、是否带日志和上传结果。它先组装 FeedbackThreadEvent;如果有原线程,就调用 enqueue_thread_feedback_event,否则调用 handle_feedback_thread_event 直接显示。

调用关系:主事件循环收到 FeedbackSubmitted 事件后会走这里。它把事件分流给线程队列或当前聊天历史。

调用图:调用 2 个内部函数(enqueue_thread_feedback_event, handle_feedback_thread_event)。

App::handle_mcp_inventory_result657–689 ↗
fn handle_mcp_inventory_result(
        &mut self,
        result: Result<Vec<McpServerStatus>, String>,
        detail: McpServerStatusDetail,
        thread_id: Option<ThreadId>,
    )

作用:处理 MCP 清单读取完成后的界面展示。它会清掉加载动画,然后显示工具列表、空状态或错误。

数据流:输入是状态列表结果、详细程度和可选线程编号。若结果属于别的当前未显示线程,就直接忽略;否则清掉加载提示。失败时加错误消息,空列表时加空状态,正常时把状态转换成 MCP 工具输出历史单元。

调用关系:主事件循环收到 McpInventoryLoaded 后调用它。它还会调用 clear_committed_mcp_inventory_loading 清理已经写进 transcript 的加载单元。

调用图:调用 1 个内部函数(clear_committed_mcp_inventory_loading);外部调用 3 个(format!, empty_mcp_output, new_mcp_tools_output_from_statuses)。

App::clear_committed_mcp_inventory_loading691–704 ↗
fn clear_committed_mcp_inventory_loading(&mut self)

作用:从聊天记录里删除已经提交进去的 MCP 清单加载提示。它避免加载完成后界面还残留“正在加载”。

数据流:它查看 transcript_cells,找到最后一个 McpInventoryLoadingCell 并删除;如果当前有 transcript 覆盖层,也同步替换覆盖层里的单元列表。

调用关系:由 App::handle_mcp_inventory_result 调用。它只做界面记录清理,不请求服务器。

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

fetch_all_mcp_server_statuses707–739 ↗
async fn fetch_all_mcp_server_statuses(
    request_handle: AppServerRequestHandle,
    detail: McpServerStatusDetail,
    thread_id: Option<ThreadId>,
) -> Result<Vec<McpServerStatus>>

作用:向 app-server 分页读取所有 MCP 服务器状态。分页就是一次拿一批,直到没有下一页。

数据流:输入是请求句柄、详细程度和可选线程编号。它循环发送 McpServerStatusList 请求,每次最多 100 条,把每页 data 追加到列表;如果响应里有 next_cursor 就继续,否则返回完整状态列表。

调用关系:由 App::fetch_mcp_inventory 的后台任务调用。它是实际和服务器通信读取 MCP 清单的函数。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_mcp_inventory);外部调用 3 个(new, String, format!)。

fetch_account_rate_limits741–752 ↗
async fn fetch_account_rate_limits(
    request_handle: AppServerRequestHandle,
) -> Result<GetAccountRateLimitsResponse>

作用:向 app-server 读取账号限流和额度信息。它是状态页和启动预取需要的底层请求。

数据流:输入是请求句柄。它生成唯一请求 ID,发送 GetAccountRateLimits 请求;成功返回服务器响应,失败附加“在 TUI 读取失败”的上下文。

调用关系:由 App::refresh_rate_limits 调用。外层负责后台任务和超时,这里只负责一次 RPC 请求。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(refresh_rate_limits);外部调用 2 个(String, format!)。

fetch_account_token_activity754–765 ↗
async fn fetch_account_token_activity(
    request_handle: AppServerRequestHandle,
) -> Result<codex_app_server_protocol::GetAccountTokenUsageResponse>

作用:向 app-server 读取账号 token 用量活动。它帮助界面展示最近使用情况。

数据流:输入是请求句柄。它生成请求 ID,发送 GetAccountTokenUsage 请求;返回 token 用量响应或带上下文的错误。

调用关系:由 App::refresh_token_activity 调用。它是该功能的实际服务器请求层。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(refresh_token_activity);外部调用 2 个(String, format!)。

fetch_rate_limit_reset_credits767–783 ↗
async fn fetch_rate_limit_reset_credits(
    request_handle: AppServerRequestHandle,
) -> Result<RateLimitResetCreditsSummary>

作用:读取账号限流信息里的重置额度摘要。它会确认服务器响应确实包含这块数据。

数据流:输入是请求句柄。它发送 GetAccountRateLimits 请求,拿到响应后取出 rate_limit_reset_credits;如果响应里没有这项,就返回明确错误。

调用关系:由 App::refresh_rate_limit_reset_credits 调用。它复用账号限流接口,但只提取重置额度部分。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(refresh_rate_limit_reset_credits);外部调用 2 个(String, format!)。

consume_rate_limit_reset_credit_request785–797 ↗
async fn consume_rate_limit_reset_credit_request(
    request_handle: AppServerRequestHandle,
    idempotency_key: String,
) -> Result<ConsumeAccountRateLimitResetCreditResponse>

作用:向 app-server 提交“使用一个限流重置额度”的请求。幂等键保证重复提交时后台能识别同一次操作。

数据流:输入是请求句柄和幂等键。它生成请求 ID,发送 ConsumeAccountRateLimitResetCredit 请求;返回消耗结果或错误。

调用关系:由 App::consume_rate_limit_reset_credit 调用。外层负责超时和事件通知。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(consume_rate_limit_reset_credit);外部调用 2 个(String, format!)。

send_add_credits_nudge_email799–813 ↗
async fn send_add_credits_nudge_email(
    request_handle: AppServerRequestHandle,
    credit_type: AddCreditsNudgeCreditType,
) -> Result<codex_app_server_protocol::AddCreditsNudgeEmailStatus>

作用:请求 app-server 发送增加额度提醒邮件,并返回发送状态。

数据流:输入是请求句柄和额度类型。它发送 SendAddCreditsNudgeEmail 请求;响应里可能有更多内容,但这个函数只取 status 返回。

调用关系:由 App::send_add_credits_nudge_email 的后台任务调用。它是同名 App 方法的底层 RPC。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(send_add_credits_nudge_email);外部调用 2 个(String, format!)。

fetch_skills_list815–832 ↗
async fn fetch_skills_list(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
) -> Result<SkillsListResponse>

作用:读取当前目录下可用的技能列表。技能元数据用于技能提及和技能界面。

数据流:输入是请求句柄和当前工作目录。它发送 SkillsList 请求,参数里包含这个目录并要求强制重载;返回技能列表响应或错误。

调用关系:由 App::refresh_startup_skills 调用。启动时它在后台跑,避免拖慢第一屏。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(refresh_startup_skills);外部调用 3 个(String, format!, vec!)。

fetch_connectors_list834–855 ↗
async fn fetch_connectors_list(
    request_handle: AppServerRequestHandle,
    force_refetch: bool,
    thread_id: Option<String>,
) -> Result<ConnectorsSnapshot>

作用:读取连接器快照。快照就是把服务器返回的连接器列表包成界面更方便使用的结构。

数据流:输入是请求句柄、是否强制刷新和可选线程编号。它发送 AppsList 请求;成功后把 response.data 放进 ConnectorsSnapshot 返回。

调用关系:由 App::fetch_connectors_list 调用。它负责具体 app/list 请求,外层负责发事件。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_connectors_list);外部调用 2 个(String, format!)。

fetch_plugins_list857–866 ↗
async fn fetch_plugins_list(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
) -> Result<PluginListResponse>

作用:读取基础插件列表,并隐藏只适合命令行内部使用的插件市场。这样用户看到的是适合 TUI 展示的列表。

数据流:输入是请求句柄和当前目录。它调用 request_plugin_list 拿原始列表,然后调用 hide_cli_only_plugin_marketplaces 过滤掉隐藏市场,最后返回处理后的响应。

调用关系:由 App::fetch_plugins_list 调用。它还被插件提及功能间接依赖,因为 request_plugin_list 也是提及刷新会用到的底层能力。

调用图:调用 2 个内部函数(hide_cli_only_plugin_marketplaces, request_plugin_list);被 1 处调用(fetch_plugins_list)。

fetch_additional_plugin_remote_sections868–919 ↗
async fn fetch_additional_plugin_remote_sections(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
    plugin_sharing_enabled: bool,
    remote_plugin_enabled: bool,
) -> (Vec<PluginMarke

作用:读取插件界面里的额外远程区块,比如工作区插件、别人分享给我的插件、OpenAI curated 等。它还会把每个区块的失败变成具体提示。

数据流:输入是请求句柄、目录、插件分享功能是否开启、远程插件功能是否开启。它先决定要请求哪些区块;逐个调用 request_plugin_list_for_kinds,成功就过滤隐藏市场并合并,失败就生成 PluginRemoteSectionError;最后返回市场列表和区块错误列表。

调用关系:由 App::fetch_plugins_list 在基础插件列表成功后调用。它依赖 plugin_remote_section_error_message 和 plugin_sharing_disabled_remote_section_error 生成用户提示。

调用图:调用 4 个内部函数(hide_cli_only_plugin_marketplaces, plugin_remote_section_error_message, plugin_sharing_disabled_remote_section_error, request_plugin_list_for_kinds);被 1 处调用(fetch_plugins_list);外部调用 5 个(clone, new, clone, format!, vec!)。

plugin_remote_section_error_message921–928 ↗
fn plugin_remote_section_error_message(label: &str, err: &str) -> String

作用:把远程插件区块的原始错误补上一句下一步建议。这样用户不只看到“失败”,还知道该怎么做。

数据流:输入是区块标签和错误文字。它调用 plugin_remote_section_error_next_step 判断是否有建议;有建议就拼到错误后面,没有就原样返回错误。

调用关系:由 fetch_additional_plugin_remote_sections 在某个远程区块加载失败时调用。

调用图:调用 1 个内部函数(plugin_remote_section_error_next_step);被 1 处调用(fetch_additional_plugin_remote_sections);外部调用 1 个(format!)。

plugin_remote_section_error_next_step930–966 ↗
fn plugin_remote_section_error_next_step(label: &str, err: &str) -> &'static str

作用:根据错误文字猜测最有帮助的处理建议,比如登录 ChatGPT、找管理员开权限、稍后重试等。

数据流:输入是区块标签和错误文字。它把错误转成小写后按关键词匹配;匹配到认证、权限、404、旧版本、服务不可用等情况时返回固定建议,否则返回空字符串。

调用关系:只被 plugin_remote_section_error_message 调用。它是远程插件错误提示的规则表。

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

plugin_sharing_disabled_remote_section_error968–974 ↗
fn plugin_sharing_disabled_remote_section_error() -> PluginRemoteSectionError

作用:生成“Shared with me”区块因为插件分享关闭而无法加载的错误。它让界面能明确指出是功能开关问题。

数据流:没有输入。它直接构造一个 PluginRemoteSectionError,section_id 是 shared-with-me,消息说明本次会话关闭了插件分享。

调用关系:由 fetch_additional_plugin_remote_sections 在插件分享功能没开启时调用。

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

hide_cli_only_plugin_marketplaces978–982 ↗
fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse)

作用:从插件市场列表里移除只给命令行内部用的市场,比如 openai-bundled。这样 TUI 不会展示不该用户直接操作的入口。

数据流:输入是可修改的 PluginListResponse。它原地过滤 marketplaces,删除名字在隐藏名单里的项;没有返回值,但响应对象会被改动。

调用关系:被 fetch_plugins_list 和 fetch_additional_plugin_remote_sections 调用,测试也会验证它确实移除了 openai-bundled。

调用图:被 3 处调用(fetch_additional_plugin_remote_sections, fetch_plugins_list, hide_cli_only_plugin_marketplaces_removes_openai_bundled)。

request_plugin_list984–990 ↗
async fn request_plugin_list(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
) -> Result<PluginListResponse>

作用:请求插件列表的简单入口,不限定插件市场类型。调用者想拿完整基础列表时用它。

数据流:输入是请求句柄和目录。它把 marketplace_kinds 设为 None,然后交给 request_plugin_list_with_marketplace_kinds;返回插件列表响应。

调用关系:被 fetch_plugins_list 和 fetch_plugin_mentions 使用。它是更通用函数 request_plugin_list_with_marketplace_kinds 的薄包装。

调用图:调用 1 个内部函数(request_plugin_list_with_marketplace_kinds);被 2 处调用(fetch_plugins_list, fetch_plugin_mentions)。

request_plugin_list_for_kinds992–998 ↗
async fn request_plugin_list_for_kinds(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
    marketplace_kinds: Vec<PluginListMarketplaceKind>,
) -> Result<PluginListResponse>

作用:按指定市场类型请求插件列表。比如只要工作区目录,或者只要分享给我的插件。

数据流:输入是请求句柄、目录和市场类型列表。它把这些类型传给 request_plugin_list_with_marketplace_kinds;返回对应插件列表。

调用关系:由 fetch_additional_plugin_remote_sections 逐个远程区块调用。

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

request_plugin_list_with_marketplace_kinds1000–1017 ↗
async fn request_plugin_list_with_marketplace_kinds(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
    marketplace_kinds: Option<Vec<PluginListMarketplaceKind>>,
) -> Result<PluginList

作用:真正发送 plugin/list 请求的通用函数。它负责校验目录必须是绝对路径。

数据流:输入是请求句柄、目录和可选市场类型。它先把目录转换为 AbsolutePathBuf,失败就报“目录必须是绝对路径”;然后生成请求 ID,发送 PluginList 请求并返回结果。

调用关系:由 request_plugin_list 和 request_plugin_list_for_kinds 调用。它是插件列表 RPC 的共同底层。

调用图:调用 2 个内部函数(request_typed, try_from);被 2 处调用(request_plugin_list, request_plugin_list_for_kinds);外部调用 3 个(String, format!, vec!)。

fetch_plugin_detail1019–1028 ↗
async fn fetch_plugin_detail(
    request_handle: AppServerRequestHandle,
    params: PluginReadParams,
) -> Result<PluginReadResponse>

作用:向 app-server 读取某个插件的详细资料。它是插件详情页的底层请求。

数据流:输入是请求句柄和 PluginReadParams。它生成请求 ID,发送 PluginRead 请求;返回插件详情响应或错误。

调用关系:由 App::fetch_plugin_detail 调用。外层负责后台运行和事件通知。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_plugin_detail);外部调用 2 个(String, format!)。

fetch_marketplace_add1030–1049 ↗
async fn fetch_marketplace_add(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
    source: String,
) -> Result<MarketplaceAddResponse>

作用:向 app-server 添加插件市场。它会先处理本地相对路径,避免后台拿到含糊不清的位置。

数据流:输入是请求句柄、当前目录和来源字符串。它先确认当前目录是绝对路径,再用 marketplace_add_source_for_request 把 ./ 或 ../ 这类相对来源解析成绝对路径;然后发送 MarketplaceAdd 请求。

调用关系:由 App::fetch_marketplace_add 调用。路径处理由 marketplace_add_source_for_request 完成。

调用图:调用 3 个内部函数(request_typed, marketplace_add_source_for_request, try_from);被 1 处调用(fetch_marketplace_add);外部调用 3 个(as_path, String, format!)。

marketplace_add_source_for_request1051–1076 ↗
fn marketplace_add_source_for_request(cwd: &std::path::Path, source: String) -> String

作用:把添加插件市场时的相对本地路径改成绝对路径,同时保留 #分支 或 @版本 这类后缀。这样后台能准确找到目录。

数据流:输入是当前目录和用户写的来源。它先拆出可能的 # 或 @ 后缀;如果主体是 .、..、./、../ 等相对路径,就按当前目录解析成绝对路径再拼回后缀;否则原样返回。

调用关系:由 fetch_marketplace_add 调用,也有测试专门验证它解析相对路径、保留后缀、不改 owner/repo 和 ~/path。

调用图:调用 1 个内部函数(resolve_path_against_base);被 2 处调用(fetch_marketplace_add, marketplace_add_source_for_request_resolves_relative_local_paths);外部调用 2 个(format!, matches!)。

fetch_marketplace_remove1078–1090 ↗
async fn fetch_marketplace_remove(
    request_handle: AppServerRequestHandle,
    marketplace_name: String,
) -> Result<MarketplaceRemoveResponse>

作用:向 app-server 删除一个插件市场。它只需要市场内部名。

数据流:输入是请求句柄和市场名。它生成请求 ID,发送 MarketplaceRemove 请求;返回删除响应或错误。

调用关系:由 App::fetch_marketplace_remove 调用。外层负责把结果带回界面。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_marketplace_remove);外部调用 2 个(String, format!)。

fetch_marketplace_upgrade1092–1104 ↗
async fn fetch_marketplace_upgrade(
    request_handle: AppServerRequestHandle,
    marketplace_name: Option<String>,
) -> Result<MarketplaceUpgradeResponse>

作用:向 app-server 请求升级插件市场。可以指定某个市场,也可以传 None 让后台升级默认范围。

数据流:输入是请求句柄和可选市场名。它生成请求 ID,发送 MarketplaceUpgrade 请求;返回升级响应或错误。

调用关系:由 App::fetch_marketplace_upgrade 调用。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_marketplace_upgrade);外部调用 2 个(String, format!)。

fetch_plugin_install1105–1123 ↗
async fn fetch_plugin_install(
    request_handle: AppServerRequestHandle,
    location: PluginLocation,
    plugin_name: String,
) -> Result<PluginInstallResponse>

作用:向 app-server 安装插件。它会把插件位置转换成后台协议需要的两个字段。

数据流:输入是请求句柄、插件位置和插件名。它调用 location.into_request_params 得到本地市场路径或远程市场名,然后发送 PluginInstall 请求;返回安装响应或错误。

调用关系:由 App::fetch_plugin_install 调用。位置拆分依赖 PluginLocation 自己的转换方法,测试会验证本地和远程只选一个位置。

调用图:调用 2 个内部函数(request_typed, into_request_params);被 1 处调用(fetch_plugin_install);外部调用 2 个(String, format!)。

fetch_plugin_uninstall1125–1137 ↗
async fn fetch_plugin_uninstall(
    request_handle: AppServerRequestHandle,
    plugin_id: String,
) -> Result<PluginUninstallResponse>

作用:向 app-server 卸载插件。它用插件 ID 精确指定要移除的插件。

数据流:输入是请求句柄和插件 ID。它生成请求 ID,发送 PluginUninstall 请求;返回卸载响应或错误。

调用关系:由 App::fetch_plugin_uninstall 调用。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(fetch_plugin_uninstall);外部调用 2 个(String, format!)。

write_plugin_enabled1139–1158 ↗
async fn write_plugin_enabled(
    request_handle: AppServerRequestHandle,
    plugin_id: String,
    enabled: bool,
) -> Result<ConfigWriteResponse>

作用:把插件启用状态写入配置。它使用 upsert,意思是有这个配置就更新,没有就创建。

数据流:输入是请求句柄、插件 ID 和开关状态。它构造 key_path 为 plugins.<插件ID>,value 为 { enabled: true/false },发送 ConfigValueWrite 请求;返回配置写入响应。

调用关系:由 App::spawn_plugin_enabled_write 调用。它是插件开关落盘的底层函数。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(spawn_plugin_enabled_write);外部调用 3 个(String, format!, json!)。

write_hook_enabled1160–1186 ↗
async fn write_hook_enabled(
    request_handle: AppServerRequestHandle,
    key: String,
    enabled: bool,
) -> Result<ConfigWriteResponse>

作用:把 Hook 启用状态批量写入配置,并要求重新加载用户配置。批量写入这里虽然只有一项,但使用统一的批量接口。

数据流:输入是请求句柄、Hook key 和开关状态。它构造对 hooks.state 的编辑,值里包含这个 key 的 enabled 状态;发送 ConfigBatchWrite 请求,成功返回写入响应。

调用关系:由 App::spawn_hook_enabled_write 调用。它是 Hook 开关落盘的底层函数。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(spawn_hook_enabled_write);外部调用 3 个(String, format!, vec!)。

build_feedback_upload_params1188–1210 ↗
fn build_feedback_upload_params(
    origin_thread_id: Option<ThreadId>,
    rollout_path: Option<PathBuf>,
    category: FeedbackCategory,
    reason: Option<String>,
    turn_id: Option<String>,

作用:把界面上的反馈信息整理成后台上传接口需要的参数。它决定是否带线程号、日志文件和 turn_id 标签。

数据流:输入是原线程编号、日志路径、反馈类别、原因、turn_id 和是否带日志。它把反馈类别转换成 classification;如果 include_logs 为真才放入日志路径;如果有 turn_id 就放进 tags;最后返回 FeedbackUploadParams。

调用关系:由 App::submit_feedback 调用,测试也验证了带日志和不带日志两种情况。

调用图:被 3 处调用(submit_feedback, build_feedback_upload_params_includes_thread_id_and_rollout_path, build_feedback_upload_params_omits_rollout_path_without_logs);外部调用 1 个(feedback_classification)。

fetch_feedback_upload1212–1221 ↗
async fn fetch_feedback_upload(
    request_handle: AppServerRequestHandle,
    params: FeedbackUploadParams,
) -> Result<FeedbackUploadResponse>

作用:向 app-server 上传反馈。它是反馈提交的实际网络请求。

数据流:输入是请求句柄和上传参数。它生成请求 ID,发送 FeedbackUpload 请求;返回上传响应或带上下文的错误。

调用关系:由 App::submit_feedback 的后台任务调用。上传完成后外层会把结果变成 FeedbackSubmitted 事件。

调用图:调用 1 个内部函数(request_typed);被 1 处调用(submit_feedback);外部调用 2 个(String, format!)。

mcp_inventory_maps_from_statuses1236–1261 ↗
fn mcp_inventory_maps_from_statuses(statuses: Vec<McpServerStatus>) -> McpInventoryMaps

作用:测试用:把扁平的 MCP 服务器状态转换成旧式的多张映射表。工具名会加上服务器名前缀,避免不同服务器的工具重名。

数据流:输入是一组 McpServerStatus。它逐个服务器提取认证状态、资源、资源模板和工具;工具键被改成 mcp__服务器名__工具名;最后返回四张 HashMap。

调用关系:只在测试配置下编译,并由 mcp_inventory_maps_prefix_tool_names_by_server 测试调用。正常 TUI 展示直接用 McpServerStatus,不走这里。

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

tests::test_absolute_path1272–1274 ↗
fn test_absolute_path(path: &str) -> AbsolutePathBuf

作用:测试辅助函数:把字符串变成 AbsolutePathBuf。它让测试构造绝对路径更方便。

数据流:输入是路径字符串。它转成 PathBuf,再尝试转成 AbsolutePathBuf;如果不是合法绝对路径,测试直接失败。

调用关系:被插件市场相关测试调用,用来准备测试数据。

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

tests::marketplace_add_source_for_request_resolves_relative_local_paths1277–1299 ↗
fn marketplace_add_source_for_request_resolves_relative_local_paths()

作用:测试添加插件市场时,相对本地路径会被解析成绝对路径,而普通仓库名不会被误改。

数据流:它根据系统选择 Windows 或 Unix 风格当前目录,调用 marketplace_add_source_for_request 多次;然后断言 ./marketplace 会变绝对路径,#main 后缀会保留,owner/repo 和 ~/marketplace 原样保留。

调用关系:它直接验证 marketplace_add_source_for_request 这个路径转换规则。

调用图:调用 1 个内部函数(marketplace_add_source_for_request);外部调用 4 个(from, assert!, assert_eq!, cfg!)。

tests::hide_cli_only_plugin_marketplaces_removes_openai_bundled1302–1333 ↗
fn hide_cli_only_plugin_marketplaces_removes_openai_bundled()

作用:测试 TUI 会隐藏 openai-bundled 这个只给命令行内部用的插件市场。

数据流:它构造一个包含 openai-bundled 和 openai-curated 的插件列表,调用 hide_cli_only_plugin_marketplaces;之后断言只剩 openai-curated。

调用关系:它验证 hide_cli_only_plugin_marketplaces 的过滤行为,防止未来改动把隐藏市场又显示出来。

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

tests::plugin_location_request_params_select_exactly_one_location1336–1353 ↗
fn plugin_location_request_params_select_exactly_one_location()

作用:测试插件安装位置转换时,本地路径和远程市场名只能二选一。这样请求不会同时带两个互相冲突的位置。

数据流:它构造本地 PluginLocation 和远程 PluginLocation,分别调用 into_request_params;断言本地只返回 marketplace_path,远程只返回 remote_marketplace_name。

调用关系:它间接保障 fetch_plugin_install 发送安装请求时位置参数清楚、不含糊。

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

tests::plugin_remote_section_error_message_adds_concrete_next_steps1356–1406 ↗
fn plugin_remote_section_error_message_adds_concrete_next_steps()

作用:测试远程插件区块错误会附上具体建议。比如需要登录、权限不对、服务不可用时,用户能看到下一步。

数据流:它准备多组区块标签、错误文字和期望建议;逐个检查 plugin_remote_section_error_message 的输出是否等于原错误加上建议。

调用关系:它验证 plugin_remote_section_error_message 和内部规则 plugin_remote_section_error_next_step 的组合效果。

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

tests::plugin_sharing_disabled_remote_section_error_targets_shared_with_me1409–1418 ↗
fn plugin_sharing_disabled_remote_section_error_targets_shared_with_me()

作用:测试插件分享关闭时,错误会准确指向“Shared with me”区块。

数据流:它调用 plugin_sharing_disabled_remote_section_error,然后断言 section_id、label 和 message 都是预期内容。

调用关系:它保障 fetch_additional_plugin_remote_sections 在插件分享关闭时生成稳定、明确的界面错误。

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

tests::mcp_inventory_maps_prefix_tool_names_by_server1421–1470 ↗
fn mcp_inventory_maps_prefix_tool_names_by_server()

作用:测试 MCP 工具名会加服务器名前缀。这样多个服务器都有 list 工具时也不会混在一起。

数据流:它构造两个 MCP 服务器状态,其中一个有 list 工具。调用 mcp_inventory_maps_from_statuses 后,断言工具键是 mcp__docs__list,并且资源、模板、认证状态按服务器保存。

调用关系:它验证测试专用转换函数 mcp_inventory_maps_from_statuses 的核心规则。

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

tests::mcp_inventory_omits_thread_id_for_closed_agent_thread1473–1490 ↗
async fn mcp_inventory_omits_thread_id_for_closed_agent_thread()

作用:测试读取 MCP 清单时,如果代理线程已经关闭,就不会再把这个线程编号发给后台。

数据流:它创建测试 App 和线程编号,先标记线程活跃且未关闭,断言 mcp_inventory_request_thread_id 返回 Some;再把线程标记关闭,断言返回 None。

调用关系:它验证 App::mcp_inventory_request_thread_id 的“关闭线程不带上下文”规则。

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

tests::build_feedback_upload_params_includes_thread_id_and_rollout_path1493–1519 ↗
fn build_feedback_upload_params_includes_thread_id_and_rollout_path()

作用:测试反馈上传参数在选择带日志时,会包含线程编号、日志路径、原因和 turn_id 标签。

数据流:它构造线程 ID、日志路径和反馈字段,调用 build_feedback_upload_params;然后逐项断言 classification、reason、thread_id、tags、include_logs 和 extra_log_files 都正确。

调用关系:它保障 App::submit_feedback 组装带日志反馈时不会漏掉关键上下文。

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

tests::build_feedback_upload_params_omits_rollout_path_without_logs1522–1538 ↗
fn build_feedback_upload_params_omits_rollout_path_without_logs()

作用:测试用户不选择带日志时,即使传入了日志路径,也不会上传日志文件。

数据流:它调用 build_feedback_upload_params,并把 include_logs 设为 false;然后断言 thread_id、tags、extra_log_files 等不该有的字段都是 None,include_logs 为 false。

调用关系:它保障反馈上传尊重用户是否附带日志的选择。

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