Codex 系统手册

审批协调的工具编排和审批 UI

stage-14.1.512 个文件

这一阶段像“安全闸门”和“前台窗口”,主要发生在系统干活过程中。后台总调度员先判断工具能不能直接跑,要不要沙箱、审批或升级重试;MCP 模板把外部工具的风险写成用户看得懂的问题。终端界面这边负责把请求排队、弹窗、显示待处理项和最近被拦下的操作,还能收集用户回答、切换权限模式、提示 Windows 沙箱问题。hooks 联络员则帮界面向后台查询和信任检查规则。整体配合起来,就是让危险动作先说明白、再等人点头。

本阶段的文件12

批准编排核心

这些文件定义批准提示内容,并驱动端到端运行时流程,用于判断工具执行何时需要用户批准或升级。

core/src/mcp_tool_approval_templates.rs源码 ↗
domain_logicrequest handling / first-use lazy load

这个文件解决的是“怎么把一次工具调用说清楚给用户看”的问题。MCP 可以理解成一套让程序调用外部工具的协议;有些工具会改日历、发评论、创建内容,这类操作需要用户先确认。文件里先从内置的 JSON 模板文件读出一组规则,每条规则说明:哪个服务器、哪个连接器、哪个工具标题,对应什么确认文案,以及哪些参数要用更友好的名字显示。真正渲染时,它要求服务器名、连接器 ID、工具标题都精确匹配,像拿钥匙开对应的锁;匹配不到就直接返回空,表示不用这个模板。它还会把模板里的“{connector_name}”替换成真实连接器名字,并把工具参数整理成展示列表。为了安全,参数标签不能空、不能重名;工具参数也必须是 JSON 对象(一种键值表),否则宁可不生成模板。

函数细节11
render_mcp_tool_approval_template53–69 ↗
fn render_mcp_tool_approval_template(
    server_name: &str,
    connector_id: Option<&str>,
    connector_name: Option<&str>,
    tool_title: Option<&str>,
    tool_params: Option<&Value>,
) -> Optio

作用:这是外部调用这个文件的主入口。别人想给一次 MCP 工具调用生成用户确认文案时,就会把服务器、连接器、工具标题和参数交给它。

数据流:进去的是服务器名、可选的连接器 ID、可选的连接器显示名、可选的工具标题、可选的工具参数。它先取出已经懒加载好的内置模板;如果模板没加载成功,就返回空。模板存在时,它把这些信息交给更具体的渲染函数,最后出来的是一份已经拼好的确认问题和参数展示,或者空。

调用关系:它被 maybe_request_mcp_tool_approval 在需要向用户请求 MCP 工具批准时调用。它自己不做细活,而是把活交给 render_mcp_tool_approval_template_from_templates,这样主流程只需要关心“能不能给我一份确认提示”。

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

load_consequential_tool_message_templates71–92 ↗
fn load_consequential_tool_message_templates() -> Option<Vec<ConsequentialToolMessageTemplate>>

作用:这个函数负责把随代码打包的确认模板 JSON 文件读出来并检查能不能用。没有它,系统就不知道哪些危险工具该用什么人话来提醒用户。

数据流:进去没有运行时参数。它通过 include_str! 把编译时打包进来的 consequential_tool_message_templates.json 当成字符串读取,再用 JSON 解析工具把它变成模板结构。然后它检查 schema_version,也就是文件格式版本号;版本不对或解析失败时会记一条警告并返回空,成功时返回模板列表。

调用关系:它是静态懒加载变量 CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES 的加载动作:第一次有人需要模板时才运行。出错时它用 warn! 记录问题,让系统不中断运行,但也不会拿坏模板去生成用户提示。

调用图:外部调用 2 个(include_str!, warn!)。

render_mcp_tool_approval_template_from_templates94–124 ↗
fn render_mcp_tool_approval_template_from_templates(
    templates: &[ConsequentialToolMessageTemplate],
    server_name: &str,
    connector_id: Option<&str>,
    connector_name: Option<&str>,
    to

作用:这个函数是真正把“模板库 + 一次工具调用信息”拼成确认提示的地方。它会严格找一条完全匹配的模板,找不到就不生成。

数据流:进去的是模板列表、服务器名、连接器 ID、连接器显示名、工具标题和工具参数。它先要求连接器 ID 和工具标题都存在,并去掉工具标题前后的空白;然后在模板列表里找服务器名、连接器 ID、工具标题全都一样的那条。找到后,它调用 render_question_template 生成问题文字,再在参数是 JSON 对象时调用 render_tool_params 整理参数展示。出来的是 RenderedMcpToolApprovalTemplate,里面有问题、消息、原始参数和适合展示的参数列表;任何一步不合格都返回空。

调用关系:render_mcp_tool_approval_template 会把实际请求转交给它。测试函数 renders_exact_match_with_readable_param_labels 和 renders_literal_template_without_connector_substitution 也直接调用它,验证精确匹配、文案替换和参数展示是否符合预期。它再把文案处理交给 render_question_template,把参数整理交给 render_tool_params。

调用图:调用 2 个内部函数(render_question_template, render_tool_params);被 3 处调用(render_mcp_tool_approval_template, renders_exact_match_with_readable_param_labels, renders_literal_template_without_connector_substitution);外部调用 2 个(new, iter)。

render_question_template126–140 ↗
fn render_question_template(template: &str, connector_name: Option<&str>) -> Option<String>

作用:这个函数把确认问题模板变成最终显示给用户的一句话。它主要处理模板里的“{connector_name}”占位符,也就是要替换成真实连接器名字的位置。

数据流:进去的是模板字符串和可选的连接器显示名。它先去掉模板前后的空白;模板为空就返回空。如果模板里包含 {connector_name},它要求连接器显示名也存在且不是空白,然后替换进去。模板不需要替换时,就原样返回整理后的文字。

调用关系:它只被 render_mcp_tool_approval_template_from_templates 调用,处在生成确认提示的中间一步。它保证最终问题不是空话,也避免出现“Allow {connector_name} ...”这种占位符没填上的尴尬文案。

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

render_tool_params142–190 ↗
fn render_tool_params(
    tool_params: &Map<String, Value>,
    template_params: &[ConsequentialToolTemplateParam],
) -> Option<(Option<Value>, Vec<RenderedMcpToolApprovalParam>)>

作用:这个函数把工具参数整理成用户能看懂的展示列表。它会把一些机器字段名换成更友好的标签,比如把 calendar_id 显示成 Calendar。

数据流:进去的是工具参数键值表和模板里定义的参数标签列表。它先按模板顺序挑出指定参数,检查标签不能为空、展示名不能重复;然后把没被模板处理的剩余参数按名字排序补到后面,用原字段名当展示名。出来的是原始参数对象副本,以及一组带字段名、值、展示名的参数展示项;如果发现展示名冲突或标签不合法,就返回空。

调用关系:它被 render_mcp_tool_approval_template_from_templates 在工具参数确实是 JSON 对象时调用。它负责最后的“把参数摆给用户看”这一步,避免同一个展示名对应两个不同参数,减少用户误解。

调用图:被 1 处调用(render_mcp_tool_approval_template_from_templates);外部调用 6 个(new, clone, get, iter, Object, new)。

tests::renders_exact_match_with_readable_param_labels200–260 ↗
fn renders_exact_match_with_readable_param_labels()

作用:这个测试确认:当服务器、连接器和工具标题完全匹配时,系统能生成正确问题,并把重要参数显示成好懂的名字。

数据流:进去的是测试里临时造出来的一条日历模板和一组工具参数。它把这些交给渲染函数,期望出来的问题是“Allow Calendar to create an event?”,参数列表里 calendar_id 和 title 使用友好标签,未在模板里声明的 timezone 也会被保留下来。

调用关系:它由测试框架运行,用来守住最主要的成功路径。它直接调用 render_mcp_tool_approval_template_from_templates,并用 assert_eq! 对比实际结果和期望结果。

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

tests::returns_none_when_no_exact_match_exists263–283 ↗
fn returns_none_when_no_exact_match_exists()

作用:这个测试确认:如果工具标题对不上,系统不会硬套一个相似模板。这样可以避免把错误的确认文案展示给用户。

数据流:进去的是一条 create_event 模板,但测试请求的是 delete_event。它执行检查后,期望结果是空,表示没有找到精确匹配的模板。

调用关系:它由测试框架运行,重点保护“必须精确匹配”这条规则。它用 assert_eq! 验证没有模板命中时返回空。

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

tests::returns_none_when_relabeling_would_collide286–312 ↗
fn returns_none_when_relabeling_would_collide()

作用:这个测试确认:如果友好标签会和其他参数名撞车,系统会放弃生成展示结果。这样用户不会看到两个看起来同名、其实含义不同的参数。

数据流:进去的是一条把 calendar_id 标成 timezone 的模板,同时原始参数里本来就有 timezone。渲染过程会发现展示名重复,最后期望返回空。

调用关系:它由测试框架运行,用来保护参数展示的安全性和清晰度。它通过 assert_eq! 检查发生重名风险时不会生成模板。

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

tests::bundled_templates_load315–317 ↗
fn bundled_templates_load()

作用:这个测试确认随程序打包的模板文件能正常加载。它防止发布时带上了坏 JSON、错版本文件,导致实际运行时没有确认模板可用。

数据流:进去没有额外输入。它读取静态懒加载模板变量的状态,并期望它是有内容的,也就是模板文件解析成功且版本号正确。

调用关系:它由测试框架运行,直接检查 CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES 的加载结果。它不关心某条模板怎么渲染,只关心打包模板整体能不能被系统识别。

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

tests::renders_literal_template_without_connector_substitution320–347 ↗
fn renders_literal_template_without_connector_substitution()

作用:这个测试确认:如果模板文字里没有“{connector_name}”占位符,就不需要提供连接器显示名,也能正常生成确认问题。

数据流:进去的是一条写死了“GitHub”的模板、没有连接器显示名、以及空参数对象。渲染后期望得到原样的问题文字,参数展示列表为空,但原始参数对象仍保留。

调用关系:它由测试框架运行,直接调用 render_mcp_tool_approval_template_from_templates。它覆盖的是“不需要替换连接器名字”的正常路径,避免函数过度要求无关信息。

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

tests::returns_none_when_connector_placeholder_has_no_value350–370 ↗
fn returns_none_when_connector_placeholder_has_no_value()

作用:这个测试确认:如果模板需要填入连接器名字,但调用方没有提供名字,系统不会生成半成品文案。

数据流:进去的是一条包含“{connector_name}”的模板,但连接器显示名是空。渲染时无法替换占位符,所以期望结果是空。

调用关系:它由测试框架运行,用来保护文案质量。它用 assert_eq! 确认占位符缺少值时不会把不完整的问题交给用户看。

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

core/src/tools/orchestrator.rs源码 ↗
orchestrationtool execution / request handling

这个文件把一次工具调用拆成几步:先看规则,决定是否需要用户或自动审查员批准;再选择沙箱。沙箱可以理解成“隔离房间”,工具只能在允许范围内读文件、联网或执行命令。然后它真正运行工具。如果第一次被沙箱拦住,它会判断原因:是网络被拦,还是文件/命令权限不够;再看当前策略是否允许二次请求、更宽松地重试。它还会记录遥测信息,也就是给系统留下“这次为什么批准、为什么失败、是否重试成功”的运行记录。网络审批也在这里串起来:有的网络请求要马上结束审批,有的可以先挂起,等工具成功后再交给后续流程处理。整体上,它像机场安检加登机调度:既不能随便放行,也不能让合法任务卡死。

函数细节7
ToolOrchestrator::new55–59 ↗
fn new() -> Self

作用:创建一个新的工具调度员,并准备好沙箱管理器。别人要运行工具前,通常先拿到这样一个调度员实例。

数据流:进去没有额外参数 → 它新建一个 SandboxManager,也就是专门挑选和控制沙箱的部件 → 出来一个 ToolOrchestrator,里面带着这个沙箱管理器,后面可以用来安排工具执行。

调用关系:它是整个流程的起点之一。handle_call、run_exec_like、intercept_apply_patch、open_session_with_sandbox 以及相关测试会先调用它,拿到调度员后再交给 ToolOrchestrator::run 去真正跑工具。

调用图:调用 1 个内部函数(new);被 5 处调用(danger_full_access_tool_attempts_do_not_enforce_managed_network, handle_call, intercept_apply_patch, run_exec_like, open_session_with_sandbox)。

ToolOrchestrator::run_attempt61–130 ↗
async fn run_attempt(
        tool: &mut T,
        req: &Rq,
        tool_ctx: &ToolCtx,
        attempt: &SandboxAttempt<'_>,
        managed_network_active: bool,
    ) -> (Result<Out, ToolError>,

作用:执行“一次尝试”:在指定沙箱里跑工具,同时处理这次运行可能涉及的网络审批。它不决定要不要重试,只负责把这一轮跑完整。

数据流:进去的是工具、请求内容、工具上下文、这次要用的沙箱设置,以及是否启用受管网络 → 它先调用 begin_network_approval 准备网络审批,再把审批的取消令牌塞进沙箱尝试信息里,然后调用工具自己的 run 真正执行 → 出来是工具结果;如果网络审批是延后模式且工具成功,还会带出一个 DeferredNetworkApproval,表示这个网络放行还要稍后收尾。

调用关系:ToolOrchestrator::run 会在第一次执行和可能的重试执行时调用它。它把具体运行交给工具实现的 run,把网络审批的开始和结束交给 begin_network_approval、finish_immediate_network_approval、finish_deferred_network_approval。

调用图:调用 3 个内部函数(begin_network_approval, finish_deferred_network_approval, finish_immediate_network_approval);外部调用 2 个(network_approval_spec, run)。

ToolOrchestrator::run132–482 ↗
async fn run(
        &mut self,
        tool: &mut T,
        req: &Rq,
        tool_ctx: &ToolCtx,
        turn_ctx: &crate::session::turn_context::TurnContext,
        approval_policy: AskForApprov

作用:这是本文件的核心流程,负责完整跑完一次工具调用:审批、选沙箱、第一次执行、必要时申请升级并重试。调用者通常只需要调它,就能得到最终结果或明确失败原因。

数据流:进去的是工具、请求、工具上下文、当前会话轮次上下文和审批策略 → 它读取文件系统沙箱策略、网络沙箱策略、权限配置、工作目录、遥测对象等信息,先判断是否跳过审批、禁止执行或必须审批;审批通过后选择初始沙箱并调用 ToolOrchestrator::run_attempt → 如果成功,就返回输出和可能挂起的网络审批;如果被沙箱拒绝,它会分析是否能解释成网络放行请求,是否允许无沙箱重试,然后可能再次审批并第二次调用 run_attempt;最后返回成功输出,或返回被拒绝、沙箱失败等错误。

调用关系:它由外层处理工具调用的代码使用,是审批系统、沙箱系统、网络策略、遥测记录和具体工具运行之间的总连接点。它会调用 ToolOrchestrator::request_approval 做审批,调用 ToolOrchestrator::reject_if_not_approved 统一处理拒绝结果,也会借助 sandbox_outcome_from_tool_error 和 build_denial_reason_from_output 给失败记录和重试理由补上可理解的信息。

调用图:调用 9 个内部函数(file_system_sandbox_policy, network_sandbox_policy, flat_tool_name, build_denial_reason_from_output, sandbox_outcome_from_tool_error, sandbox_override_for_first_attempt, unsandboxed_execution_allowed, select_initial, from_abs_path);外部调用 18 个(now, reject_if_not_approved, request_approval, run_attempt, escalate_on_failure, exec_approval_requirement, sandbox_cwd, sandbox_permissions, sandbox_preference, should_bypass_approval (+8 more))。

ToolOrchestrator::request_approval487–549 ↗
async fn request_approval(
        tool: &mut T,
        req: &Rq,
        permission_request_run_id: &str,
        approval_ctx: ApprovalCtx<'_>,
        tool_ctx: &ToolCtx,
        evaluate_permissi

作用:发起一次权限审批,问“这次工具能不能做”。它会先让配置里的权限钩子自动回答;如果没有答案,再走正常的用户审批或自动审查员审批。

数据流:进去的是工具、请求、这次审批的编号、审批上下文、工具上下文、是否运行权限钩子,以及遥测记录器 → 它先看工具能不能提供权限请求内容;如果可以并且允许跑钩子,就调用 run_permission_request_hooks,钩子说允许就返回 Approved,说拒绝就直接变成 ToolError::Rejected → 如果钩子没给答案,它调用工具自己的 start_approval_async 发起真实审批,并把审批来源和结果写进遥测 → 出来是 ReviewDecision,或者拒绝错误。

调用关系:ToolOrchestrator::run 在第一次需要审批时、以及沙箱失败后需要升级重试时会调用它。它把“自动配置决定”和“用户/Guardian 自动审查员决定”统一成同一种审批结果,方便后面的 reject_if_not_approved 判断。

调用图:调用 3 个内部函数(run_permission_request_hooks, flat_tool_name, tool_decision);外部调用 3 个(permission_request_payload, start_approval_async, Rejected)。

ToolOrchestrator::reject_if_not_approved551–578 ↗
async fn reject_if_not_approved(
        tool_ctx: &ToolCtx,
        guardian_review_id: Option<&str>,
        decision: ReviewDecision,
    ) -> Result<(), ToolError>

作用:把审批结果翻译成“能继续”或“立刻停止”。它专门负责处理拒绝、超时、网络策略修订等不同审批结果,避免主流程到处写重复判断。

数据流:进去的是工具上下文、可选的 Guardian 审查编号、审批结果 → 如果结果是拒绝或中止,它会生成拒绝原因;如果有 Guardian 编号,就向 guardian_rejection_message 取更具体的自动审查拒绝说明,否则用“rejected by user” → 如果是超时,就返回 guardian_timeout_message;如果是批准或允许网络策略修订,就返回成功;如果网络策略修订是拒绝,就返回拒绝错误。

调用关系:ToolOrchestrator::run 每次拿到审批结果后都会用它做最后把关。它不发起审批,只负责把 ReviewDecision 变成后续流程能懂的 Ok 或 ToolError::Rejected。

调用图:外部调用 3 个(Rejected, guardian_rejection_message, guardian_timeout_message)。

sandbox_outcome_from_tool_error581–588 ↗
fn sandbox_outcome_from_tool_error(err: &ToolError) -> Option<&'static str>

作用:从工具错误里提取一个简短的沙箱结果标签,比如“被拒绝”“超时”“收到信号退出”。这些标签主要给遥测记录使用。

数据流:进去的是一个 ToolError → 它只查看错误类型,不改动任何东西;如果错误来自沙箱拒绝、沙箱超时或沙箱信号,就分别返回固定英文标签 → 如果是普通拒绝或其他 Codex 错误,就返回 None,表示没有可记录的沙箱结果。

调用关系:ToolOrchestrator::run 在第一次尝试失败或重试失败后调用它。它的结果会交给遥测系统,用来记录这次沙箱运行到底是怎么结束的。

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

build_denial_reason_from_output590–594 ↗
fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String

作用:为“沙箱里失败了,要不要不带沙箱重试?”生成一条简短稳定的说明。现在它故意不分析复杂输出,只给用户一个清楚的重试理由。

数据流:进去的是工具执行输出 → 目前它不读取输出细节,只保留这个参数位置,方便以后再做更聪明的判断 → 出来固定字符串“command failed; retry without sandbox?”,供审批请求展示。

调用关系:ToolOrchestrator::run 在沙箱拒绝但不是明确网络拦截时调用它,用这句话作为升级审批的 retry_reason。它让审批提示保持简短,也让测试和用户体验更稳定。

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

批准事件整理

这些文件规范化传入的批准相关信号,并维护精简摘要,供 UI 稍后渲染或引用。

tui/src/approval_events.rs源码 ↗
domain_logicrequest handling

这个文件解决的是“审批提示怎么在终端界面里被保存和展示”的问题。比如系统想执行一条命令,或者想改几个文件,TUI 需要先把请求记下来,等当前输出流结束或轮到它显示时,再问用户同不同意。ExecApprovalRequestEvent 表示“执行命令”的审批请求,里面有命令、工作目录、原因、可选的权限放宽方案等信息。ApplyPatchApprovalRequestEvent 表示“应用补丁改文件”的审批请求,里面保存哪些文件会怎么变。文件里还藏着一段很重要的小规则:如果后端没有明确告诉界面有哪些按钮可选,就根据请求内容自己推断默认选项。比如涉及网络访问时,会提供“允许一次”“本会话允许”“取消”等选择;如果只是普通命令,就可能提供“允许”“按建议放宽执行策略后允许”“取消”。这样界面不会因为后端少给了按钮列表就没法问用户。

函数细节3
ExecApprovalRequestEvent::effective_approval_id45–49 ↗
fn effective_approval_id(&self) -> String

作用:这个函数给一次执行审批找出真正该用的审批编号。它的作用是保证即使请求里没有单独的 approval_id,也仍然能用 call_id 当作稳定的编号。

数据流:进去的是一个 ExecApprovalRequestEvent。它先看 approval_id 有没有值;有的话就复制这个值出来,没有的话就复制 call_id。出来的是一个字符串编号,不会改动原来的请求。

调用关系:它通常会在 TUI 需要把用户的批准或拒绝结果对应回某个请求时派上用场。它不把工作交给别的内部函数,只是在请求自己的两个字段之间做一个兜底选择。

ExecApprovalRequestEvent::effective_available_decisions51–61 ↗
fn effective_available_decisions(&self) -> Vec<CommandExecutionApprovalDecision>

作用:这个函数决定界面应该给用户显示哪些审批按钮。后端如果已经给了按钮列表,它就照用;如果没给,它就根据请求内容算出一套合理默认选项。

数据流:进去的是一个 ExecApprovalRequestEvent。它先读取 available_decisions;如果这里有现成列表,就复制后返回。否则它会把网络审批信息、执行策略放宽建议、网络策略放宽建议、额外权限信息取出来,交给 default_available_decisions 计算。出来的是一组 CommandExecutionApprovalDecision,也就是用户可以点的决定项;原请求不会被修改。

调用关系:它位于“准备展示审批提示”的中间层:界面想知道该画哪些按钮时会用它。它把复杂的默认规则交给 ExecApprovalRequestEvent::default_available_decisions,这样调用者不用自己理解各种权限场景。

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

ExecApprovalRequestEvent::default_available_decisions63–106 ↗
fn default_available_decisions(
        network_approval_context: Option<&NetworkApprovalContext>,
        proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
        proposed_network_policy_

作用:这个函数是在后端没明确给出按钮列表时,用来生成默认审批选项的规则表。它根据请求是否涉及网络、是否有额外权限、是否建议放宽执行策略,拼出用户能选择的几个结果。

数据流:进去的是几类可选信息:网络审批上下文、执行策略修改建议、网络策略修改建议、额外权限配置。它先判断是不是网络相关请求;如果是,就生成“允许一次”“本会话允许”,必要时再加“应用网络放宽规则”,最后加“取消”。如果有额外权限请求,它只给“允许”和“取消”。其他普通执行请求会先给“允许”,如果有执行策略放宽建议就加上对应选项,最后加“取消”。出来的是一个决定列表;它只生成新列表,不改任何输入。

调用关系:它是 ExecApprovalRequestEvent::effective_available_decisions 的后备方案:只有请求自己没有带 available_decisions 时才会被调用。它内部主要是在创建列表,也就是通过 vec! 这类列表构造工具把各个按钮选项按顺序装起来。

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

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

在这个项目里,有些命令、改文件、联网、申请权限等动作会先经过自动审查。如果审查结果是“拒绝”,终端界面需要把这些拒绝记录暂时存起来,之后展示给用户看。这个文件就做这件事。RecentAutoReviewDenials 里面用 VecDeque(一种两头都能快速增删的队列)保存记录,新记录放到最前面,旧记录往后排,而且最多只留 10 条,避免界面被过期信息塞满。它还会按 id 去重:同一个审查事件如果又来了,旧的先删掉,再放最新的。action_summary 则负责把不同类型的动作翻译成一句人能看懂的话,比如“执行这个命令”“访问这个网络地址”“修改这些文件”。文件底部的测试确认最关键的规则:超过 10 条时,只留下最新的 10 条。

函数细节7
RecentAutoReviewDenials::push15–23 ↗
fn push(&mut self, event: GuardianAssessmentEvent)

作用:把一条自动审查事件放进“最近拒绝记录”里,但只收真正被拒绝的事件。它还会去掉同 id 的旧记录,并保证最多只保留 10 条最新记录。

数据流:输入是一条 GuardianAssessmentEvent,也就是一次自动审查的结果。函数先看它的 status,如果不是 Denied(被拒绝)就直接不管;如果是拒绝,就从现有队列里删掉同 id 的旧项,再把新事件插到最前面,最后截断队列,只留下最近 10 条。结果是内部 entries 被更新,没有直接返回值。

调用关系:当系统收到新的自动审查结果时,会用这个函数更新界面要展示的拒绝记录。它内部把具体存储工作交给队列的 retain、push_front 和 truncate:先清旧的,再放新的,再裁掉多余的。

调用图:外部调用 3 个(push_front, retain, truncate)。

RecentAutoReviewDenials::is_empty25–27 ↗
fn is_empty(&self) -> bool

作用:检查现在有没有任何被拒绝的自动审查记录。界面可以用它决定要不要显示相关提示区域。

数据流:输入是当前 RecentAutoReviewDenials 自己。函数读取内部 entries 队列,问它是不是空的,然后返回 true 或 false;它不会改动任何记录。

调用关系:它通常会被展示层或流程判断调用,用来快速决定“这里有没有东西可显示”。实际判断工作交给底层队列的 is_empty。

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

RecentAutoReviewDenials::entries29–31 ↗
fn entries(&self) -> impl Iterator<Item = &GuardianAssessmentEvent>

作用:按当前保存的顺序拿到所有拒绝记录,通常用于界面逐条显示。因为新记录放在最前面,所以读出来也是从最新到较旧。

数据流:输入是当前 RecentAutoReviewDenials。函数读取内部 entries,并返回一个迭代器(可以一个接一个读取内容的东西),让调用者查看每条 GuardianAssessmentEvent;它不复制整份列表,也不修改内容。

调用关系:当界面要渲染最近拒绝列表时,会调用它拿到记录。它把逐条读取的工作交给 VecDeque 的 iter。

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

RecentAutoReviewDenials::take33–36 ↗
fn take(&mut self, id: &str) -> Option<GuardianAssessmentEvent>

作用:按 id 找到一条拒绝记录,并把它从列表里取走。适合用户处理完某条提示后,把它从待显示记录中移除。

数据流:输入是一个 id 字符串引用。函数先在 entries 里逐条找 id 相同的记录;找不到就返回 None;找到了就把那一项从队列中删除并返回 Some(event)。结果是内部列表少了一条对应记录。

调用关系:它通常用于“消费”某条拒绝事件,比如用户点击查看或系统决定不再展示它时。它先用 iter 找位置,再用 remove 真正把记录拿出来。

调用图:外部调用 2 个(iter, remove)。

action_summary39–75 ↗
fn action_summary(action: &GuardianAssessmentAction) -> String

作用:把一项被审查的动作变成简短、可读的一句话。这样界面不用直接展示复杂的数据结构,而是能告诉用户“这个动作大概想做什么”。

数据流:输入是 GuardianAssessmentAction,也就是被审查的具体动作。函数根据动作类型分别处理:命令就返回命令文本;Execve(直接启动程序)就把程序和参数拼成类似 shell 命令的一行;ApplyPatch(打补丁改文件)就说明改了哪个文件或多少个文件;联网就写目标地址;MCP 工具调用就写工具和服务;申请权限就写申请理由。输出是一个 String,也就是最终展示用的摘要文字。

调用关系:它是界面展示拒绝记录时的翻译器。RecentAutoReviewDenials 保存的是完整事件,而显示给人看时可以用这个函数提炼动作说明;它内部会用 format! 组织文字,也会用 shlex::try_join 尽量把程序参数拼成安全、像命令行的一句话。

调用图:外部调用 3 个(format!, try_join, vec!)。

tests::denied_event86–104 ↗
fn denied_event(id: usize) -> GuardianAssessmentEvent

作用:为测试造一条假的“被拒绝审查事件”。这样测试不用每次手写一大段重复数据。

数据流:输入是一个数字 id。函数用这个数字拼出 review-id、拒绝理由和一条类似 rm -rf /tmp/test-id 的命令,并设置状态为 Denied。输出是一条完整的 GuardianAssessmentEvent,供测试塞进 RecentAutoReviewDenials。

调用关系:它只在测试里服务于 tests::keeps_only_ten_most_recent_denials。为了构造命令所在目录,它调用 test_path_buf 生成测试用路径,也用 format! 拼出不同的 id 和命令文本。

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

tests::keeps_only_ten_most_recent_denials107–132 ↗
fn keeps_only_ten_most_recent_denials()

作用:验证 RecentAutoReviewDenials 最多只保留 10 条最新拒绝记录。这是在保护一个重要行为:旧消息不能无限堆积,也不能把最新消息挤掉。

数据流:测试先创建一个空的 RecentAutoReviewDenials,然后连续塞入 12 条假的拒绝事件。之后它读取当前 entries 的 id 列表,并断言结果正好是 review-11 到 review-2,也就是最新的 10 条;最早的 review-0 和 review-1 应该已经被丢掉。

调用关系:这是对 RecentAutoReviewDenials::push 和 RecentAutoReviewDenials::entries 配合效果的检查。它用 tests::denied_event 生成测试数据,用 assert_eq! 确认实际顺序和数量都符合预期。

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

批准与输入覆盖层

这些底部窗格组件提供主要的交互式批准和结构化响应界面,并为其他位置的待处理批准提供轻量可见性。

tui/src/bottom_pane/approval_overlay.rs源码 ↗
orchestrationrequest handling

这个文件像一个门卫窗口。后台代理想做可能有风险的事时,会发来一个 ApprovalRequest,这里把它变成一个可上下选择的列表,显示理由、命令、权限范围、线程来源等信息。用户按快捷键或回车后,ApprovalOverlay 会把选择翻译成明确的审批事件,例如“同意执行这条命令”“本次不给权限”“取消 MCP 请求”,再通过 AppEventSender 发回应用。它还处理多个审批排队、请求已经被后台解决时自动关掉弹窗、Ctrl-C 或取消键中止当前请求等情况。一个特别重要的细节是 MCP elicitation(外部服务向用户要补充信息)里,Esc 永远表示 Cancel,避免用户以为在安全退出,实际却变成“继续但不给信息”。

函数细节82
ApprovalRequest::thread_id109–116 ↗
fn thread_id(&self) -> ThreadId

作用:从任意一种审批请求里取出它属于哪个对话线程。这样回复审批结果时,主程序知道该把结果送回哪条任务线。

数据流:进去的是一个审批请求 → 它按请求类型查看里面保存的 thread_id → 出来的是同一个 ThreadId,不改动请求。

调用关系:审批弹窗在发送命令审批、权限回复、补丁审批和 MCP 回复时会需要这个线程编号,它是后续事件能投递到正确线程的基础。

ApprovalRequest::thread_label118–125 ↗
fn thread_label(&self) -> Option<&str>

作用:取出请求的线程名称,也就是给人看的来源标签。它用来提示用户“这个审批来自哪条线程”。

数据流:进去的是一个审批请求 → 它查看请求里有没有 thread_label → 出来是可选的文字引用;没有标签就返回空。

调用关系:approval_footer_hint 会用它判断要不要显示“打开来源线程”的快捷键;处理决定时也会用它判断是否需要写入当前历史记录。

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

ApprovalRequest::matches_resolved_request127–154 ↗
fn matches_resolved_request(&self, request: &ResolvedAppServerRequest) -> bool

作用:判断一个正在显示或排队的审批请求,是否就是后台已经解决掉的那个请求。这样可以关掉过期弹窗,避免用户再对不存在的请求做选择。

数据流:进去的是当前 ApprovalRequest 和一个已解决请求 → 它按类型比较对应的 id、call_id、server_name、request_id → 出来是真或假,不改动数据。

调用关系:ApprovalOverlay::dismiss_resolved_request 会用它清理当前请求和队列里的请求,保证界面不会留下旧审批。

ApprovalOverlay::new172–193 ↗
fn new(
        request: ApprovalRequest,
        app_event_tx: AppEventSender,
        features: Features,
        approval_keymap: ApprovalKeymap,
        list_keymap: ListKeymap,
    ) -> Self

作用:创建一个新的审批弹窗,并立刻把第一条请求显示出来。外部有新审批要展示时会调用它。

数据流:进去的是审批请求、事件发送器、功能开关和快捷键配置 → 它先建一个空弹窗,再调用 set_current 把请求转成列表界面 → 出来是可使用的 ApprovalOverlay。

调用关系:它被显示延迟审批、推入审批请求以及测试辅助函数调用;内部会创建 ListSelectionView,并把后续真正的界面构建交给 set_current。

调用图:调用 1 个内部函数(new);被 4 处调用(maybe_show_delayed_approval_requests_at, push_approval_request, apply_patch_prompt_with_thread_label_omits_command_line, make_overlay_with_keymap);外部调用 4 个(default, new, clone, clone)。

ApprovalOverlay::enqueue_request195–197 ↗
fn enqueue_request(&mut self, req: ApprovalRequest)

作用:把新的审批请求放到队伍后面,等当前这个处理完再显示。这样多个风险操作不会互相覆盖。

数据流:进去的是一个 ApprovalRequest → 它把请求 push 到 queue → 弹窗内部队列多了一项,没有返回值。

调用关系:BottomPaneView 的 try_consume_approval_request 会调用它;当前请求结束后 advance_queue 会从这个队列里取下一个。

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

ApprovalOverlay::dismiss_resolved_request199–214 ↗
fn dismiss_resolved_request(&mut self, request: &ResolvedAppServerRequest) -> bool

作用:当后台告诉界面某个审批已经解决时,把对应的弹窗或队列项移除。它不会额外发送拒绝或取消,避免误伤。

数据流:进去的是已解决请求 → 它先从队列里删除匹配项,再检查当前显示项是否匹配;如果当前项匹配就标记完成并前进队列 → 出来是真或假,表示有没有移除东西。

调用关系:dismiss_app_server_request 会调用它;如果当前请求被移除,它会交给 advance_queue 显示下一条或结束弹窗。

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

ApprovalOverlay::set_current216–230 ↗
fn set_current(&mut self, request: ApprovalRequest)

作用:把某个审批请求设置为当前正在显示的请求,并重新生成标题、说明和选项列表。它是换题目的地方。

数据流:进去的是一个 ApprovalRequest → 它生成头部说明 build_header,再生成选项 build_options,最后重建 ListSelectionView → 弹窗状态变成显示这个新请求。

调用关系:new 和 advance_queue 会用它;它把“请求数据”转换成“用户能看的列表界面”。

调用图:调用 2 个内部函数(build_header, new);被 1 处调用(advance_queue);外部调用 3 个(build_options, clone, clone)。

ApprovalOverlay::build_options232–300 ↗
fn build_options(
        request: &ApprovalRequest,
        header: Box<dyn Renderable>,
        _features: &Features,
        approval_keymap: &ApprovalKeymap,
        list_keymap: &ListKeymap,

作用:根据请求类型生成用户能点的选项,比如“同意一次”“本会话都同意”“拒绝”。同时准备列表视图需要的标题、页脚提示和条目。

数据流:进去的是审批请求、头部渲染对象、功能开关和快捷键配置 → 它按 Exec、Permissions、ApplyPatch、McpElicitation 分别调用不同的选项生成函数 → 出来是一组 ApprovalOption 和 SelectionViewParams。

调用关系:set_current 调用它;它会调用 exec_options、permissions_options、patch_options、elicitation_options 和 approval_footer_hint,把业务选择包装成通用列表组件能显示的内容。

调用图:调用 6 个内部函数(approval_footer_hint, elicitation_options, exec_options, patch_options, permissions_options, with);外部调用 4 个(new, default, from, format!)。

ApprovalOverlay::apply_selection302–347 ↗
fn apply_selection(&mut self, actual_idx: usize)

作用:用户选中某一行后,把这一行代表的决定真正执行出去。它是“点了按钮之后发生什么”的核心。

数据流:进去的是选项下标 → 它找到对应 ApprovalOption,检查它和当前请求类型是否匹配,然后调用相应 handle_*_decision 发送结果 → 标记当前请求完成,并推进队列。

调用关系:快捷键处理和列表回车选择都会调用它;它把具体发送工作分给 handle_exec_decision、handle_permissions_decision、handle_patch_decision 或 handle_elicitation_decision。

调用图:调用 5 个内部函数(advance_queue, handle_elicitation_decision, handle_exec_decision, handle_patch_decision, handle_permissions_decision);被 2 处调用(handle_key_event, try_handle_shortcut)。

ApprovalOverlay::handle_exec_decision349–386 ↗
fn handle_exec_decision(
        &self,
        id: &str,
        command: &[String],
        decision: CommandExecutionApprovalDecision,
    )

作用:处理“是否执行命令”的决定,并把结果发回应用。必要时还会在历史记录里留下一条“用户批准/拒绝了什么”。

数据流:进去的是审批 id、命令和命令审批决定 → 它判断是否要写历史,识别普通命令或网络访问目标,再发送 InsertHistoryCell 和 exec_approval → 结果是主程序收到命令审批答案。

调用关系:apply_selection 在用户选择命令审批选项时调用它;cancel_current_request 取消命令审批时也调用它。它会用 network_approval_target、network_approval_command_target 和 command_decision_to_review_decision。

调用图:调用 5 个内部函数(exec_approval, send, command_decision_to_review_decision, network_approval_command_target, network_approval_target);被 2 处调用(apply_selection, cancel_current_request);外部调用 3 个(InsertHistoryCell, new_approval_decision_cell, Command)。

ApprovalOverlay::handle_permissions_decision388–436 ↗
fn handle_permissions_decision(
        &self,
        call_id: &str,
        permissions: &RequestPermissionProfile,
        decision: PermissionsDecision,
    )

作用:处理“是否授予额外权限”的决定,例如只给本轮、给整个会话、或不给。它把人的选择变成程序能理解的权限回复。

数据流:进去的是 call_id、请求的权限范围和用户决定 → 它决定实际授予哪些权限、作用范围是本轮还是会话、是否启用严格自动审查 → 发送权限回复事件,并可能写一条历史消息。

调用关系:apply_selection 和 cancel_current_request 会调用它;它最后通过 AppEventSender::request_permissions_response 把结果交回对应线程。

调用图:调用 3 个内部函数(request_permissions_response, send, new);被 2 处调用(apply_selection, cancel_current_request);外部调用 6 个(new, default, clone, InsertHistoryCell, matches!, vec!)。

ApprovalOverlay::handle_patch_decision438–448 ↗
fn handle_patch_decision(&self, id: &str, decision: FileChangeApprovalDecision)

作用:处理“是否允许修改文件”的决定。它把同意或取消补丁应用的结果发回主程序。

数据流:进去的是补丁审批 id 和文件改动决定 → 它从当前请求取 thread_id,再调用 patch_approval → 出来是主程序收到文件改动审批结果。

调用关系:apply_selection 在用户选择补丁选项时调用它;cancel_current_request 取消补丁时也调用它。

调用图:调用 1 个内部函数(patch_approval);被 2 处调用(apply_selection, cancel_current_request)。

ApprovalOverlay::handle_elicitation_decision450–471 ↗
fn handle_elicitation_decision(
        &self,
        server_name: &str,
        request_id: &RequestId,
        decision: McpServerElicitationAction,
    )

作用:处理 MCP 服务请求用户补充信息时的选择,比如接受、拒绝继续或取消。这里目前只发送决定,不附带用户填写内容。

数据流:进去的是服务名、请求 id 和 MCP 决定 → 它取当前线程 id,调用 resolve_elicitation,并把 content 和 meta 设为空 → MCP 请求得到明确答复。

调用关系:apply_selection 和 cancel_current_request 会调用它;它是 MCP elicitation 弹窗和后台协议之间的桥。

调用图:调用 1 个内部函数(resolve_elicitation);被 2 处调用(apply_selection, cancel_current_request);外部调用 1 个(clone)。

ApprovalOverlay::advance_queue473–479 ↗
fn advance_queue(&mut self)

作用:当前审批结束后,切到下一条排队请求;如果没有下一条,就把弹窗标记为完成。

数据流:进去的是当前弹窗状态 → 它从 queue 里 pop 一个请求;有就 set_current,没有就 done=true → 弹窗要么显示下一条,要么结束。

调用关系:apply_selection 和 dismiss_resolved_request 会调用它;它负责让多条审批连续出现。

调用图:调用 1 个内部函数(set_current);被 2 处调用(apply_selection, dismiss_resolved_request)。

ApprovalOverlay::cancel_current_request481–525 ↗
fn cancel_current_request(&mut self)

作用:取消当前审批,并清空后面的等待队列。它用于 Ctrl-C、取消键等“我不想继续这个审批流程”的情况。

数据流:进去的是弹窗当前状态 → 如果当前请求还没完成,它按请求类型发送对应的 Cancel 或 Deny;然后清空 queue 并设置 done=true → 弹窗关闭,后台收到安全的中止结果。

调用关系:on_ctrl_c 和 try_handle_shortcut 会调用它;它内部复用各个 handle_*_decision,确保取消也走正常回传通道。

调用图:调用 4 个内部函数(handle_elicitation_decision, handle_exec_decision, handle_patch_decision, handle_permissions_decision);被 2 处调用(on_ctrl_c, try_handle_shortcut)。

ApprovalOverlay::try_handle_shortcut531–566 ↗
fn try_handle_shortcut(&mut self, key_event: &KeyEvent) -> bool

作用:先处理审批弹窗自己的快捷键,比如全屏查看、打开来源线程、取消、直接按 y/d 选择。它避免这些键被普通列表导航抢走。

数据流:进去的是一次按键事件 → 它检查是否匹配全屏、打开线程、取消或某个选项快捷键 → 如果处理了就发送事件或应用选择并返回 true,否则返回 false。

调用关系:handle_key_event 第一时间调用它;若它没处理,才把按键交给 ListSelectionView。

调用图:调用 3 个内部函数(send, apply_selection, cancel_current_request);被 1 处调用(handle_key_event);外部调用 2 个(FullScreenApprovalRequest, SelectAgentThread)。

ApprovalOverlay::handle_key_event570–578 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)

作用:接收用户在审批弹窗里的按键。它把快捷键和列表选择两条路统一起来。

数据流:进去的是 KeyEvent → 先让 try_handle_shortcut 尝试处理;没处理就交给内部列表移动或选择,再读取列表最后选中的下标并 apply_selection → 可能发送审批结果或只改变高亮行。

调用关系:这是 BottomPaneView 接口的一部分,主界面有键盘输入时会调用它;它连接快捷键层、列表组件和审批决定发送层。

调用图:调用 4 个内部函数(apply_selection, try_handle_shortcut, handle_key_event, take_last_selected_index)。

ApprovalOverlay::on_ctrl_c580–583 ↗
fn on_ctrl_c(&mut self) -> CancellationEvent

作用:处理 Ctrl-C。对审批弹窗来说,Ctrl-C 表示取消当前请求,而不是让它悄悄悬着。

数据流:进去的是当前弹窗状态 → 它调用 cancel_current_request → 返回 CancellationEvent::Handled,表示这次取消已被弹窗处理。

调用关系:主界面收到 Ctrl-C 时通过 BottomPaneView 调用它;它把具体取消逻辑交给 cancel_current_request。

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

ApprovalOverlay::is_complete585–587 ↗
fn is_complete(&self) -> bool

作用:告诉外部这个审批弹窗是否已经结束。界面可以据此把它移走。

数据流:进去的是弹窗状态 → 读取 done 字段 → 出来是真或假,不改变任何东西。

调用关系:主界面和测试会用它判断弹窗生命周期;advance_queue 或 cancel_current_request 会改变这个结果。

ApprovalOverlay::try_consume_approval_request589–595 ↗
fn try_consume_approval_request(
        &mut self,
        request: ApprovalRequest,
    ) -> Option<ApprovalRequest>

作用:当弹窗已经打开时,再来的审批请求由它接收并排队,而不是新开一个弹窗。

数据流:进去的是新 ApprovalRequest → 它调用 enqueue_request 放入队列 → 返回 None,表示请求已经被当前弹窗吃掉了。

调用关系:这是 BottomPaneView 接口的一部分;它把新请求转交给 enqueue_request,后面由 advance_queue 显示。

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

ApprovalOverlay::dismiss_app_server_request597–599 ↗
fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool

作用:让外部通知弹窗:某个服务器请求已经结束,可以从界面上撤掉。它防止过期审批继续出现。

数据流:进去的是 ResolvedAppServerRequest → 它调用 dismiss_resolved_request → 出来是真或假,表示是否确实消除了某个请求。

调用关系:这是 BottomPaneView 接口的一部分;实际匹配和换队列由 dismiss_resolved_request 完成。

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

ApprovalOverlay::terminal_title_requires_action601–603 ↗
fn terminal_title_requires_action(&self) -> bool

作用:告诉终端标题:当前弹窗需要用户操作。这样用户即使没盯着界面,也能知道有事要确认。

数据流:进去的是弹窗状态 → 固定返回 true → 不读取复杂状态,也不改动数据。

调用关系:主界面更新终端标题时会看这个接口;审批弹窗始终代表需要用户注意。

ApprovalOverlay::desired_height607–609 ↗
fn desired_height(&self, width: u16) -> u16

作用:询问这个弹窗在给定宽度下希望占多少行。界面布局需要它来安排空间。

数据流:进去的是宽度 → 它把问题转交给内部 ListSelectionView → 出来是高度行数。

调用关系:渲染前和测试辅助 render_overlay_lines 会调用它;它不自己算高度,而是信任内部列表组件。

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

ApprovalOverlay::render611–613 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把审批弹窗画到终端缓冲区里。也就是让用户真正看到标题、说明和选项。

数据流:进去的是绘制区域和缓冲区 → 它调用内部列表的 render → 缓冲区被填上弹窗内容,没有返回值。

调用关系:主界面绘制底部面板时会调用它;测试也通过 render_overlay_lines 检查显示结果。

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

ApprovalOverlay::cursor_pos615–617 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>

作用:询问弹窗里光标应该放在哪里。虽然审批列表通常不需要输入文字,但统一渲染接口需要这个能力。

数据流:进去的是绘制区域 → 它转问内部列表组件 → 出来是可选的光标坐标。

调用关系:这是 Renderable 接口的一部分;ApprovalOverlay 只是把请求代理给 ListSelectionView。

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

network_approval_target645–660 ↗
fn network_approval_target(
    network_approval_context: &NetworkApprovalContext,
    command: &[String],
) -> String

作用:为网络访问审批生成一个给人看的目标地址。优先从命令里拿真实目标,拿不到再用协议和主机名拼出来。

数据流:进去的是网络审批上下文和命令参数 → 它先调用 network_approval_command_target;如果没有,就根据 http/https/socks5 等协议拼 host → 出来是目标字符串。

调用关系:handle_exec_decision 写历史记录时调用它,让历史里显示“访问了哪个网络目标”。

调用图:调用 1 个内部函数(network_approval_command_target);被 1 处调用(handle_exec_decision);外部调用 1 个(format!)。

network_approval_command_target662–672 ↗
fn network_approval_command_target(command: &[String]) -> Option<&str>

作用:识别命令是不是特殊的 network-access 命令,并从里面取出目标地址。这样历史记录可以更像人话。

数据流:进去的是命令字符串数组 → 它匹配两种写法:两个参数的 network-access target,或单字符串前缀 network-access → 出来是可选目标文本。

调用关系:handle_exec_decision 和 network_approval_target 都会调用它;它负责把内部命令形式翻译成用户能懂的网络目标。

调用图:被 2 处调用(handle_exec_decision, network_approval_target)。

build_header674–800 ↗
fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable>

作用:为不同审批请求生成弹窗上半部分的说明文字。它决定用户在做决定前能看到哪些上下文。

数据流:进去的是 ApprovalRequest → 它按类型加入线程、原因、环境、权限规则、命令片段、服务器消息等内容,并包装成可渲染对象 → 出来是 header。

调用关系:set_current 调用它;它会用权限格式化函数、命令清理和命令高亮函数,把原始请求变成清楚的说明区。

调用图:调用 5 个内部函数(format_additional_permissions_rule, format_requested_permissions_rule, strip_bash_lc_and_escape, highlight_bash_to_lines, with);被 1 处调用(set_current);外部调用 7 个(new, from, from_iter, new, from, new, vec!)。

command_decision_to_review_decision825–844 ↗
fn command_decision_to_review_decision(
    decision: &CommandExecutionApprovalDecision,
) -> ReviewDecision

作用:把命令审批协议里的决定,转换成历史记录使用的决定类型。两边表达的是同一件事,但格式不同。

数据流:进去的是 CommandExecutionApprovalDecision → 它逐项匹配同意、会话同意、策略变更、拒绝、取消 → 出来是 ReviewDecision。

调用关系:handle_exec_decision 写审批历史时调用它;它让历史组件不用直接理解后台审批协议。

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

exec_options846–932 ↗
fn exec_options(
    available_decisions: &[CommandExecutionApprovalDecision],
    network_approval_context: Option<&NetworkApprovalContext>,
    additional_permissions: Option<&AdditionalPermissionPr

作用:根据后台允许的命令审批决定,生成用户可见的选项文字和快捷键。它会针对普通命令、网络访问、额外权限给出不同措辞。

数据流:进去的是可用决定列表、可选网络上下文、可选额外权限和快捷键表 → 它逐个决定生成 ApprovalOption,并过滤掉不适合显示的项,比如多行命令前缀 → 出来是一组选项。

调用关系:build_options 会调用它;多个测试也直接调用它,确认网络审批和普通命令的按钮文案正确。

调用图:被 4 处调用(build_options, additional_permissions_exec_options_hide_execpolicy_amendment, generic_exec_options_can_offer_allow_for_session, network_exec_options_use_expected_labels_and_hide_execpolicy_amendment);外部调用 1 个(iter)。

format_additional_permissions_rule934–983 ↗
fn format_additional_permissions_rule(
    additional_permissions: &AdditionalPermissionProfile,
) -> Option<String>

作用:把额外权限范围整理成一行人能读懂的话,比如“network; read /tmp/a; write /tmp/b”。

数据流:进去的是 AdditionalPermissionProfile → 它检查网络是否开启,再把文件系统读、写、拒绝读的条目分别格式化 → 没有权限返回 None,有则返回拼好的字符串。

调用关系:build_header 用它展示命令请求附带的额外权限;format_requested_permissions_rule 也复用它。

调用图:调用 1 个内部函数(format_file_system_entry_paths);被 2 处调用(build_header, format_requested_permissions_rule);外部调用 2 个(new, format!)。

format_requested_permissions_rule985–996 ↗
fn format_requested_permissions_rule(
    permissions: &RequestPermissionProfile,
) -> Option<String>

作用:把“请求权限”的格式先转换成“已授予权限”的统一格式,再生成人能读懂的一行规则。

数据流:进去的是 RequestPermissionProfile → 它调用转换函数得到 AdditionalPermissionProfile 的形态,再调用 format_additional_permissions_rule → 出来是可选的规则字符串。

调用关系:build_header 在权限审批弹窗中调用它,让用户看到到底要授予网络或文件访问的哪些范围。

调用图:调用 2 个内部函数(granted_permission_profile_from_request, format_additional_permissions_rule);被 1 处调用(build_header);外部调用 1 个(clone)。

format_file_system_entry_paths998–1009 ↗
fn format_file_system_entry_paths(
    entries: impl Iterator<Item = &'a FileSystemSandboxEntry>,
) -> String

作用:把一组文件系统权限条目变成逗号分隔的路径说明。它支持普通路径、通配符和特殊位置。

数据流:进去的是文件系统沙盒条目的迭代器 → 它把每个条目按路径类型转成带反引号的文字,特殊位置交给 special_path_label → 出来是一个字符串。

调用关系:format_additional_permissions_rule 会调用它分别处理读、写、拒绝读的文件范围。

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

special_path_label1011–1020 ↗
fn special_path_label(value: &FileSystemSpecialPath) -> String

作用:把内部的特殊路径名字翻译成用户能看懂的标签,比如根目录、临时目录、工作区根目录。

数据流:进去的是 FileSystemSpecialPath → 它按枚举类型返回固定标签,带子路径的情况调用 path_label 拼接 → 出来是字符串。

调用关系:format_file_system_entry_paths 在遇到特殊路径时会间接用到它;它让权限显示不暴露难懂的内部结构。

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

path_label1022–1027 ↗
fn path_label(base: &str, subpath: &Option<PathBuf>) -> String

作用:把一个基础路径标签和可选子路径拼成一段显示文字。没有子路径时只显示基础标签。

数据流:进去的是 base 和可选 PathBuf → 有子路径就拼成 base/subpath,没有就返回 base → 出来是路径标签字符串。

调用关系:special_path_label 用它处理工作区根目录和未知特殊路径中带子路径的情况。

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

patch_options1029–1047 ↗
fn patch_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption>

作用:生成文件修改审批的三个选项:同意一次、本会话这些文件不再问、取消并让 Codex 改做法。

数据流:进去的是审批快捷键配置 → 它组装三个 ApprovalOption,分别绑定补丁审批决定和快捷键 → 出来是选项数组。

调用关系:build_options 在 ApplyPatch 请求时调用它。

调用图:被 1 处调用(build_options);外部调用 1 个(vec!)。

permissions_options1049–1081 ↗
fn permissions_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption>

作用:生成权限审批的选项,包括本轮授权、严格自动审查授权、整场会话授权、拒绝。它还特意不把 Esc 作为“拒绝继续”的快捷键。

数据流:进去的是审批快捷键配置 → 它过滤掉 deny 里的 Esc,再组装四个权限选项 → 出来是 ApprovalOption 列表。

调用关系:build_options 在 Permissions 请求时调用它;测试会直接检查这些文案和快捷键行为。

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

elicitation_options1090–1122 ↗
fn elicitation_options(keymap: &ApprovalKeymap) -> Vec<ApprovalOption>

作用:生成 MCP 补充信息请求的选项,并强制保证 Esc 是取消。这样用户按 Esc 永远是安全中止,不会变成“继续但不给信息”。

数据流:进去的是审批快捷键配置 → 它把 Esc 和 cancel 快捷键合并成取消快捷键,再从 decline 快捷键里移除这些冲突键 → 出来是接受、拒绝继续、取消三个选项。

调用关系:build_options 在 McpElicitation 请求时调用它;相关测试确认自定义快捷键冲突时 Esc 仍然取消。

调用图:被 1 处调用(build_options);外部调用 1 个(vec!)。

tests::absolute_path1141–1143 ↗
fn absolute_path(path: &str) -> AbsolutePathBuf

作用:测试里用的小工具,把字符串变成绝对路径类型。这样测试数据更接近真实权限里的路径。

数据流:进去的是路径字符串 → 它调用 from_absolute_path 校验并构造 AbsolutePathBuf → 出来是绝对路径对象;非法路径会让测试失败。

调用关系:多个测试辅助和快照测试会用它准备 /tmp/readme.txt 等路径。

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

tests::render_overlay_lines1145–1159 ↗
fn render_overlay_lines(view: &ApprovalOverlay, width: u16) -> String

作用:把审批弹窗渲染成普通字符串,方便测试比较界面文字。它相当于把终端画面拍成文本快照。

数据流:进去的是 ApprovalOverlay 和宽度 → 它计算高度、创建缓冲区、调用 render,再逐行取出字符并去掉行尾空格 → 出来是多行字符串。

调用关系:多个快照测试调用它;它依赖 ApprovalOverlay::desired_height 和 ApprovalOverlay::render。

调用图:调用 2 个内部函数(desired_height, render);外部调用 2 个(empty, new)。

tests::render_history_cell_lines1161–1174 ↗
fn render_history_cell_lines(
        cell: &dyn crate::history_cell::HistoryCell,
        width: u16,
    ) -> Vec<String>

作用:把历史记录单元格转成纯文本行,方便测试审批历史写得是否正确。

数据流:进去的是 HistoryCell 和宽度 → 它调用 display_lines,再把每行的 span 内容拼起来 → 出来是字符串数组。

调用关系:历史记录相关测试用它检查用户批准命令、网络访问等文案。

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

tests::normalize_snapshot_paths1176–1185 ↗
fn normalize_snapshot_paths(rendered: String) -> String

作用:把快照里的绝对路径统一成稳定文本,避免不同环境显示路径差异导致测试乱失败。

数据流:进去的是渲染字符串 → 它把测试构造出的绝对路径替换成固定的 /tmp/... 文本 → 出来是规范化后的字符串。

调用关系:权限弹窗快照测试会调用它;它内部用 absolute_path 构造要替换的路径。

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

tests::make_overlay1187–1200 ↗
fn make_overlay(
        request: ApprovalRequest,
        app_event_tx: AppEventSender,
        features: Features,
    ) -> ApprovalOverlay

作用:测试里快速创建使用默认快捷键的审批弹窗。减少每个测试重复写初始化代码。

数据流:进去的是请求、事件发送器和功能开关 → 它取默认运行时快捷键,再调用 make_overlay_with_keymap → 出来是 ApprovalOverlay。

调用关系:大多数交互测试用它创建弹窗;真正创建逻辑交给 make_overlay_with_keymap。

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

tests::make_overlay_with_keymap1202–1216 ↗
fn make_overlay_with_keymap(
        request: ApprovalRequest,
        app_event_tx: AppEventSender,
        features: Features,
        approval_keymap: ApprovalKeymap,
        list_keymap: ListKeyma

作用:测试里创建带自定义快捷键的审批弹窗。用于验证用户改键后行为仍正确。

数据流:进去的是请求、发送器、功能开关和两套快捷键 → 它直接调用 ApprovalOverlay::new → 出来是弹窗。

调用关系:自定义取消键、打开线程键、MCP Esc 冲突等测试会用它。

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

tests::make_exec_request1218–1232 ↗
fn make_exec_request() -> ApprovalRequest

作用:生成一个简单的命令审批请求,命令是 echo hi。测试用它作为常见命令审批样本。

数据流:进去没有参数 → 它生成新线程 id、请求 id、命令、理由和可用决定 → 出来是 ApprovalRequest::Exec。

调用关系:很多测试用它快速搭建命令审批场景。

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

tests::make_permissions_request1234–1251 ↗
fn make_permissions_request() -> ApprovalRequest

作用:生成一个请求网络权限和读写 /tmp 文件的权限审批样本。用于测试权限弹窗和回复。

数据流:进去没有参数 → 它构造网络开启、读路径、写路径和理由 → 出来是 ApprovalRequest::Permissions。

调用关系:权限选项、权限快照、会话授权和严格审查测试会用它。

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

tests::make_elicitation_request1253–1261 ↗
fn make_elicitation_request() -> ApprovalRequest

作用:生成一个 MCP 服务要补充信息的测试请求。用于验证接受、拒绝、取消这些行为。

数据流:进去没有参数 → 它创建线程 id、服务名、请求 id 和消息 → 出来是 ApprovalRequest::McpElicitation。

调用关系:MCP 取消键和 Esc 行为测试会用它。

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

tests::ctrl_c_aborts_and_clears_queue1264–1272 ↗
fn ctrl_c_aborts_and_clears_queue()

作用:测试 Ctrl-C 会取消当前审批,并清空后续队列。它保证用户中止时不会留下排队请求继续弹出。

数据流:进去是测试创建的弹窗和两个命令请求 → 它排队一个请求后调用 on_ctrl_c → 断言队列为空且弹窗完成。

调用关系:它验证 ApprovalOverlay::on_ctrl_c、cancel_current_request 和队列清理的配合。

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

tests::configured_list_cancel_aborts_exec_approval1275–1303 ↗
fn configured_list_cancel_aborts_exec_approval()

作用:测试自定义列表取消键也能取消命令审批。用户把取消键改成 q 时,行为仍应安全。

数据流:进去是改过 cancel 键的测试弹窗 → 它模拟按 q → 从事件通道里找 ExecApproval,并断言决定是 Cancel。

调用关系:它验证 handle_key_event 经过 try_handle_shortcut 后会走 cancel_current_request。

调用图:调用 3 个内部函数(with_defaults, new, defaults);外部调用 7 个(Char, new, assert!, assert_eq!, make_exec_request, make_overlay_with_keymap, vec!)。

tests::configured_list_cancel_cancels_mcp_elicitation1306–1334 ↗
fn configured_list_cancel_cancels_mcp_elicitation()

作用:测试自定义列表取消键对 MCP 请求也是 Cancel。这样不会因为改键而变成普通拒绝。

数据流:进去是 MCP 弹窗和 cancel=q 的键位 → 它模拟按 q → 读取 ResolveElicitation 事件并确认决定是 Cancel。

调用关系:它覆盖 try_handle_shortcut 的取消分支和 handle_elicitation_decision。

调用图:调用 3 个内部函数(with_defaults, new, defaults);外部调用 7 个(Char, new, assert!, assert_eq!, make_elicitation_request, make_overlay_with_keymap, vec!)。

tests::shortcut_triggers_selection1337–1352 ↗
fn shortcut_triggers_selection()

作用:测试直接按审批快捷键能提交选择。用户不用一定移动列表再回车。

数据流:进去是默认命令审批弹窗 → 它模拟按 y → 检查事件队列里出现线程操作事件。

调用关系:它验证 try_handle_shortcut 能匹配选项快捷键,并调用 apply_selection。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 6 个(Char, new, assert!, matches!, make_exec_request, make_overlay)。

tests::deny_shortcut_submits_denied_exec_decision1355–1391 ↗
fn deny_shortcut_submits_denied_exec_decision()

作用:测试命令审批里的拒绝快捷键会提交 Decline,而不是取消或无反应。

数据流:进去是带 Accept 和 Decline 选项的命令请求 → 它模拟按 d → 读取 ExecApproval 事件并检查 decision 是 Decline。

调用关系:它验证 exec_options 生成的 deny 快捷键和 handle_exec_decision 的发送结果。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 6 个(Char, new, assert!, assert_eq!, make_overlay, vec!)。

tests::network_deny_shortcut_submits_policy_deny_decision1394–1447 ↗
fn network_deny_shortcut_submits_policy_deny_decision()

作用:测试网络审批里“永久阻止这个主机”的快捷键会提交网络策略拒绝。它保证 d 键在该场景下指向正确的策略变更。

数据流:进去是带 Deny 网络策略变更的网络审批请求 → 它模拟按 d → 读取 ExecApproval 并比较策略变更内容。

调用关系:它覆盖 exec_options 对 ApplyNetworkPolicyAmendment::Deny 的选项生成和 apply_selection 的路由。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 6 个(Char, new, assert!, assert_eq!, make_overlay, vec!)。

tests::resolved_request_dismisses_overlay_without_emitting_abort1450–1468 ↗
fn resolved_request_dismisses_overlay_without_emitting_abort()

作用:测试后台已解决的请求会关闭弹窗,但不会额外发送取消审批。这样不会把正常解决误报成用户取消。

数据流:进去是一个命令审批弹窗 → 它调用 dismiss_app_server_request → 断言弹窗完成且事件通道没有审批操作。

调用关系:它验证 dismiss_resolved_request 和 advance_queue 的无副作用关闭行为。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 3 个(assert!, make_exec_request, make_overlay)。

tests::o_opens_source_thread_for_cross_thread_approval1471–1500 ↗
fn o_opens_source_thread_for_cross_thread_approval()

作用:测试跨线程审批时,按默认 o 键会跳到来源线程。用户可以查看这次审批从哪里来。

数据流:进去是带 thread_label 的命令审批 → 它模拟按 o → 从事件通道读取 SelectAgentThread 并检查线程 id。

调用关系:它验证 try_handle_shortcut 的 open_thread 分支。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 5 个(Char, new, assert_eq!, make_overlay, vec!)。

tests::configured_open_thread_shortcut_opens_source_thread1503–1541 ↗
fn configured_open_thread_shortcut_opens_source_thread()

作用:测试打开来源线程的快捷键可以自定义。默认 o 被改成 x 后,只有 x 生效。

数据流:进去是 open_thread=x 的跨线程审批 → 它先按 o 确认无事件,再按 x 并确认 SelectAgentThread 事件出现 → 验证改键生效。

调用关系:它验证 ApprovalKeymap.open_thread 被 try_handle_shortcut 正确使用。

调用图:调用 4 个内部函数(with_defaults, new, new, defaults);外部调用 5 个(Char, new, assert!, make_overlay_with_keymap, vec!)。

tests::exec_prefix_option_emits_execpolicy_amendment1572–1621 ↗
fn exec_prefix_option_emits_execpolicy_amendment()

作用:测试“以后同意这类命令前缀”的选项会发送执行策略修改。它保证一次审批能变成后续少询问的规则。

数据流:进去是包含 AcceptWithExecpolicyAmendment 的命令请求 → 它模拟按 p → 检查 ExecApproval 事件里的策略修改内容。

调用关系:它验证 exec_options 生成前缀选项,apply_selection 和 handle_exec_decision 能把决定原样发出。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 6 个(Char, new, assert!, assert_eq!, make_overlay, vec!)。

tests::network_deny_forever_shortcut_is_not_bound1624–1660 ↗
fn network_deny_forever_shortcut_is_not_bound()

作用:测试某些网络审批场景里隐藏的“永久拒绝”快捷键不会误触发。没有显示的选项就不该能按键提交。

数据流:进去是只允许网络 allow 策略的请求 → 它按 d → 断言事件通道没有审批事件。

调用关系:它保护 exec_options 的快捷键绑定逻辑,避免看不见的拒绝路径被触发。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 5 个(Char, new, assert!, make_overlay, vec!)。

tests::header_includes_command_snippet1663–1701 ↗
fn header_includes_command_snippet()

作用:测试普通命令审批的头部会显示命令片段。用户必须能看到要执行什么,才谈得上审批。

数据流:进去是 echo hello world 请求 → 它渲染弹窗缓冲区 → 检查任一行包含命令文本。

调用关系:它验证 build_header 对 Exec 且非网络审批的显示逻辑。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 5 个(empty, new, assert!, make_overlay, vec!)。

tests::network_exec_options_use_expected_labels_and_hide_execpolicy_amendment1704–1737 ↗
fn network_exec_options_use_expected_labels_and_hide_execpolicy_amendment()

作用:测试网络审批选项使用网络专用文案,并且不显示普通命令前缀规则选项。

数据流:进去是网络上下文和一组可用决定 → 它直接调用 exec_options → 比较生成的 label 列表。

调用关系:它验证 exec_options 在 network_approval_context 存在时的分支。

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

tests::generic_exec_options_can_offer_allow_for_session1740–1762 ↗
fn generic_exec_options_can_offer_allow_for_session()

作用:测试普通命令审批可以提供“本会话不再问这条命令”的选项。

数据流:进去是普通命令可用决定 → 调用 exec_options → 比较生成文案。

调用关系:它验证 exec_options 在没有网络和额外权限时的普通命令文案。

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

tests::additional_permissions_exec_options_hide_execpolicy_amendment1765–1795 ↗
fn additional_permissions_exec_options_hide_execpolicy_amendment()

作用:测试带额外权限的命令审批不会错误显示命令策略前缀类文案。权限审批和命令策略不是一回事。

数据流:进去是带文件权限的命令审批条件 → 调用 exec_options → 断言只生成同意和取消文案。

调用关系:它覆盖 exec_options 对 additional_permissions 的文案选择。

调用图:调用 3 个内部函数(from_read_write_roots, exec_options, defaults);外部调用 2 个(assert_eq!, vec!)。

tests::permissions_options_use_expected_labels1798–1813 ↗
fn permissions_options_use_expected_labels()

作用:测试权限审批四个选项的文案保持预期。按钮文字变错会直接影响用户理解。

数据流:进去是默认快捷键 → 调用 permissions_options → 抽取 label 并比较。

调用关系:它直接保护 permissions_options 的输出。

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

tests::additional_permissions_rule_shows_non_path_file_system_entries1816–1844 ↗
fn additional_permissions_rule_shows_non_path_file_system_entries()

作用:测试权限规则能显示非普通路径的文件条目,比如根目录和 glob 通配符。这样复杂权限也不会显示成空白。

数据流:进去是包含特殊路径和 glob 的权限资料 → 调用 format_additional_permissions_rule → 比较字符串。

调用关系:它验证 format_file_system_entry_paths 和 special_path_label 对特殊情况的处理。

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

tests::additional_permissions_rule_uses_workspace_roots_label1847–1869 ↗
fn additional_permissions_rule_uses_workspace_roots_label()

作用:测试工作区根目录特殊路径会显示成 :workspace_roots。这个标签比内部枚举名字更适合给人看。

数据流:进去是 ProjectRoots 带 .git 子路径的权限 → 调用 format_additional_permissions_rule → 检查输出是 read :workspace_roots/.git

调用关系:它验证 special_path_label 和 path_label 的组合。

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

tests::permissions_session_shortcut_submits_session_scope1872–1895 ↗
fn permissions_session_shortcut_submits_session_scope()

作用:测试权限审批的会话授权快捷键会把 scope 设为 Session。也就是这次同意能持续到本会话结束。

数据流:进去是权限审批弹窗 → 它模拟按 a → 读取权限回复事件并断言 scope 是 Session。

调用关系:它验证 permissions_options 的会话选项和 handle_permissions_decision 的范围转换。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 6 个(Char, new, assert!, assert_eq!, make_overlay, make_permissions_request)。

tests::permissions_deny_shortcut_uses_deny_keymap1898–1932 ↗
fn permissions_deny_shortcut_uses_deny_keymap()

作用:测试权限拒绝使用 deny 快捷键配置。用户改了拒绝键后,权限弹窗也应该跟着改。

数据流:进去是 deny=x 的权限弹窗 → 它模拟按 x → 检查返回权限为空、scope 是 Turn、没有严格审查。

调用关系:它验证 permissions_options 的 deny_shortcuts 过滤和 handle_permissions_decision 的拒绝结果。

调用图:调用 3 个内部函数(with_defaults, new, defaults);外部调用 8 个(Char, new, new, assert!, assert_eq!, make_overlay_with_keymap, make_permissions_request, vec!)。

tests::permissions_strict_auto_review_shortcut_submits_turn_scope_with_strict_review1935–1959 ↗
fn permissions_strict_auto_review_shortcut_submits_turn_scope_with_strict_review()

作用:测试按 r 会提交“本轮授权并启用严格自动审查”。这是权限同意的一种更谨慎模式。

数据流:进去是权限审批弹窗 → 它模拟按 r → 检查权限回复 scope 是 Turn 且 strict_auto_review 为 true。

调用关系:它验证 permissions_options 里硬编码的 r 快捷键和 handle_permissions_decision 的严格审查标志。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 6 个(Char, new, assert!, assert_eq!, make_overlay, make_permissions_request)。

tests::additional_permissions_prompt_shows_permission_rule_line1962–2015 ↗
fn additional_permissions_prompt_shows_permission_rule_line()

作用:测试带额外权限的命令审批会显示 Permission rule 行。用户要知道自己除了运行命令还在批准哪些权限。

数据流:进去是带网络和文件权限的命令请求 → 它渲染弹窗 → 检查画面包含 Permission rule 和 network。

调用关系:它验证 build_header 会调用 format_additional_permissions_rule 并显示结果。

调用图:调用 4 个内部函数(with_defaults, from_read_write_roots, new, new);外部调用 5 个(empty, new, assert!, make_overlay, vec!)。

tests::additional_permissions_prompt_snapshot2018–2051 ↗
fn additional_permissions_prompt_snapshot()

作用:用快照测试带额外权限的命令审批界面整体长什么样。防止布局和文案被无意改坏。

数据流:进去是带理由、网络、读写文件权限的命令请求 → 渲染并规范化路径 → 与快照比较。

调用关系:它覆盖 build_header、build_options、format_additional_permissions_rule 和 render_overlay_lines 的整体配合。

调用图:调用 4 个内部函数(with_defaults, from_read_write_roots, new, new);外部调用 3 个(assert_snapshot!, make_overlay, vec!)。

tests::permissions_prompt_snapshot2054–2062 ↗
fn permissions_prompt_snapshot()

作用:用快照测试权限审批弹窗的整体显示。它确认原因、权限规则和选项都在预期位置。

数据流:进去是 make_permissions_request 创建的请求 → 渲染并规范化路径 → 与快照比较。

调用关系:它覆盖权限请求的 build_header、permissions_options 和渲染链路。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 3 个(assert_snapshot!, make_overlay, make_permissions_request)。

tests::apply_patch_prompt_with_thread_label_omits_command_line2065–2095 ↗
fn apply_patch_prompt_with_thread_label_omits_command_line()

作用:测试带线程标签的补丁审批显示线程来源,但不会显示伪造的 apply_patch 命令行。

数据流:进去是一个 ApplyPatch 请求和文件变更 → 它创建弹窗并渲染 → 断言包含线程名和打开线程提示,但不包含 $ apply_patch。

调用关系:它验证 build_header 对 ApplyPatch 的显示策略,以及 approval_footer_hint 的跨线程提示。

调用图:调用 5 个内部函数(with_defaults, new, new, new, defaults);外部调用 5 个(new, from, assert!, absolute_path, render_overlay_lines)。

tests::network_exec_prompt_title_includes_host2098–2155 ↗
fn network_exec_prompt_title_includes_host()

作用:测试网络审批标题会直接写出目标主机,并且不显示普通命令行和普通命令策略文案。

数据流:进去是访问 example.com 的网络审批请求 → 它渲染弹窗并做快照 → 检查标题包含主机,且不包含 $ curl 和 don't ask again。

调用关系:它验证 build_options 的网络标题、build_header 对网络审批隐藏命令、exec_options 的网络文案。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 6 个(empty, new, assert!, assert_snapshot!, make_overlay, vec!)。

tests::ctrl_shift_a_opens_fullscreen2158–2176 ↗
fn ctrl_shift_a_opens_fullscreen()

作用:测试全屏查看审批的快捷键会发出 FullScreenApprovalRequest。用户可以把复杂审批放大看。

数据流:进去是命令审批弹窗 → 它模拟 Ctrl+Shift+A → 从事件队列里确认出现全屏审批请求事件。

调用关系:它验证 try_handle_shortcut 的 open_fullscreen 分支。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 6 个(Char, new, assert!, matches!, make_exec_request, make_overlay)。

tests::exec_history_cell_wraps_with_two_space_indent2179–2207 ↗
fn exec_history_cell_wraps_with_two_space_indent()

作用:测试命令审批历史在窄宽度下换行时有两个空格缩进。这样长命令读起来不会乱。

数据流:进去是一条较长 git add 命令的历史单元 → 它按宽度 28 渲染行 → 比较每一行文字。

调用关系:它主要保护 history_cell 的显示效果,但场景来自 handle_exec_decision 会创建的历史记录。

调用图:外部调用 4 个(assert_eq!, new_approval_decision_cell, Command, vec!)。

tests::exec_history_cell_does_not_render_blank_action_for_empty_command2210–2230 ↗
fn exec_history_cell_does_not_render_blank_action_for_empty_command()

作用:测试空命令的审批历史不会显示空白动作。没有命令时应该说“批准了这个请求”,而不是留一个难看的空洞。

数据流:进去是空命令的两种批准历史单元 → 渲染成文本行 → 分别比较一次批准和会话批准的文案。

调用关系:它保护 history_cell 在 ApprovalOverlay 写历史时可能遇到空命令的边界情况。

调用图:外部调用 4 个(new, assert_eq!, new_approval_decision_cell, Command)。

tests::network_access_command_history_uses_target_without_structured_context2233–2274 ↗
fn network_access_command_history_uses_target_without_structured_context()

作用:测试即使没有结构化网络上下文,只要命令形如 network-access target,历史也会显示目标地址。

数据流:进去是 network-access https://example.com:8443 命令审批 → 它按 y 同意 → 读取 InsertHistoryCell 并比较历史文本。

调用关系:它验证 handle_exec_decision 会调用 network_approval_command_target,把特殊命令识别成网络访问历史。

调用图:调用 3 个内部函数(with_defaults, new, new);外部调用 5 个(Char, new, assert_eq!, make_overlay, vec!)。

tests::esc_cancels_mcp_elicitation2277–2296 ↗
fn esc_cancels_mcp_elicitation()

作用:测试 MCP 补充信息弹窗里按 Esc 会取消请求。这个安全语义非常重要。

数据流:进去是 MCP elicitation 弹窗 → 它模拟按 Esc → 读取 ResolveElicitation 事件并确认 decision 是 Cancel。

调用关系:它验证 elicitation_options 和 try_handle_shortcut 对 Esc 的硬性取消行为。

调用图:调用 2 个内部函数(with_defaults, new);外部调用 4 个(new, assert_eq!, make_elicitation_request, make_overlay)。

tests::esc_still_cancels_elicitation_with_custom_overlap2299–2360 ↗
fn esc_still_cancels_elicitation_with_custom_overlap()

作用:测试即使用户把 Esc 配成 decline,MCP 弹窗里 Esc 仍然是 Cancel;同时其他 decline 键仍能拒绝继续。

数据流:进去是 decline 包含 Esc 和 n、cancel 为 x 的自定义键位 → 第一轮按 Esc 检查 Cancel,第二轮按 n 检查 Decline → 两种行为都符合预期。

调用关系:它直接验证 elicitation_options 移除冲突快捷键的规则,防止 Esc 语义被自定义键破坏。

调用图:调用 3 个内部函数(with_defaults, new, defaults);外部调用 6 个(Char, new, assert_eq!, make_elicitation_request, make_overlay_with_keymap, vec!)。

tests::enter_sets_last_selected_index_without_dismissing2363–2386 ↗
fn enter_sets_last_selected_index_without_dismissing()

作用:测试按 Enter 选择当前高亮项后,审批会完成并发送对应决定。列表本身不直接关闭,而是交给 ApprovalOverlay 完成流程。

数据流:进去是默认命令审批弹窗 → 它模拟按 Enter → 断言弹窗完成,并检查 ExecApproval 决定是 Accept。

调用关系:它验证 handle_key_event 先让列表记录选择下标,再由 ApprovalOverlay::apply_selection 发送审批结果。

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

tui/src/bottom_pane/request_user_input/mod.rs源码 ↗
orchestrationrequest handling / main loop

可以把这个文件想成一个小型问卷窗口。模型或后台需要用户补充信息时,它会把问题显示在终端底部:有选项的问题可以上下选择,也可以加备注;没有选项的问题就直接输入文字。它还负责键盘操作、粘贴、切换题目、保存草稿、提交答案、提示还有几题没答,以及在超时自动提交空答案。里面的 RequestUserInputOverlay 是核心零件,像问卷的“控制台”:一边控制显示内容,一边接收按键,一边把最终答案发成 AppEvent 给外层系统。ChatComposer 是文字输入框,ScrollState 记住列表选中哪一项,queue 保存排队来的后续问题请求。没有这层逻辑,用户可能会丢草稿、提交错题、无法中断,或者多个请求挤在一起乱掉。 从这段可以看出,这个文件关心的是一个终端弹窗:程序问用户几个问题,用户可以用方向键、数字键、回车、Tab、Esc 等操作来选答案、写备注或退出。它还处理“自动解决”,也就是用户一段时间不操作时自动提交空答案,避免界面一直卡住。测试覆盖了很多容易出错的边角:倒计时是否显示为红色,按键或粘贴后是否暂停自动提交,旧请求被服务端解决后是否正确关闭或切到下一个请求,未确认的草稿是否不会被误提交,以及小窗口、长选项、页脚提示换行时界面是否仍然清楚。可以把它理解成收银台的问答牌:既要显示问题,也要记住用户到底确认了什么,还要在用户取消或后面来了新问题时不乱账。 从这段代码能看出,这个文件关心的是一个终端里的临时提问面板。可以把它想成聊天窗口底部突然弹出的一张小表单:有时让用户从几个选项里选一个,有时让用户自己打一段话,有时还要连续回答多个问题。这里的测试主要用“快照测试”,也就是把界面画出来后和一张标准截图对比,防止以后改代码时把提示文字、滚动效果、页脚说明或快捷键提示弄坏。它还特别检查了窄窗口、小高度、选项被遮住、快捷键重新绑定、未答完就提交等容易出错的情况。这样做的价值是:终端界面没有鼠标和复杂布局,一点点显示错位都会让用户不知道该按什么、选什么。

函数细节165
format_auto_resolution_remaining83–94 ↗
fn format_auto_resolution_remaining(remaining: Duration) -> String

作用:把自动处理还剩多久,变成用户能看懂的文字,比如“8s”或“1m 05s”。它用于倒计时提示。

数据流:输入一个 Duration(时间长度)→ 先把不足 1 秒的零头向上算成 1 秒,再按是否超过 60 秒决定显示秒或分秒 → 输出一段倒计时字符串,不改任何状态。

调用关系:它被倒计时文案生成逻辑使用,帮助 RequestUserInputOverlay::auto_resolution_countdown_text_at 显示“auto-resolves in ...”。

调用图:外部调用 3 个(as_secs, subsec_nanos, format!)。

ComposerDraft::text_with_pending105–119 ↗
fn text_with_pending(&self) -> String

作用:拿到输入框草稿的完整文字,包括还没正式展开的粘贴内容。这样提交时不会漏掉用户刚粘贴进去的东西。

数据流:读取草稿里的普通文字、文本元素和等待处理的粘贴内容 → 如果没有待处理粘贴就直接返回文字,否则调用输入框的展开逻辑把粘贴内容补进去 → 输出完整文本,不修改草稿。

调用关系:提交答案时 RequestUserInputOverlay::submit_answers 会用它,确保备注或自由输入题提交的是用户实际看到和输入的内容。

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

FooterTip::new140–145 ↗
fn new(text: impl Into<String>) -> Self

作用:创建一条普通的底部操作提示,比如“enter to submit”。

数据流:输入提示文字 → 转成 String,并标记为不高亮 → 输出 FooterTip。

调用关系:RequestUserInputOverlay::footer_tips 会用它拼出底部快捷键提示列表。

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

FooterTip::highlighted147–152 ↗
fn highlighted(text: impl Into<String>) -> Self

作用:创建一条需要突出显示的底部提示,比如当前最重要的“提交”提示。

数据流:输入提示文字 → 转成 String,并标记为高亮 → 输出 FooterTip。

调用关系:RequestUserInputOverlay::footer_tips 会在关键动作上用它,让用户更容易看到下一步该按什么。

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

RequestUserInputOverlay::new179–194 ↗
fn new(
        request: ToolRequestUserInputParams,
        app_event_tx: AppEventSender,
        has_input_focus: bool,
        enhanced_keys_supported: bool,
        disable_paste_burst: bool,

作用:用默认快捷键创建一个用户输入弹窗。测试和普通调用通常走这个入口。

数据流:输入一份用户输入请求、事件发送器、输入焦点和键盘能力等设置 → 加上默认 RuntimeKeymap(运行时快捷键配置)后交给 new_with_keymap → 输出配置好的弹窗对象。

调用关系:它是 new_with_keymap 的简化包装;多个自动处理相关测试也通过它创建弹窗。

调用图:调用 1 个内部函数(defaults);被 65 处调用(auto_resolution_absent_has_no_timer, auto_resolution_expiry_emits_empty_answer, auto_resolution_hides_timer_during_grace_period, auto_resolution_key_interaction_snoozes_timer, auto_resolution_paste_interaction_snoozes_timer, auto_resolution_resets_for_queued_request, auto_resolution_visible_countdown_is_red, auto_resolution_visible_countdown_snapshot, backspace_in_options_clears_selection, backspace_on_empty_notes_closes_notes_ui (+15 more));外部调用 1 个(new_with_keymap)。

RequestUserInputOverlay::new_with_keymap196–238 ↗
fn new_with_keymap(
        request: ToolRequestUserInputParams,
        app_event_tx: AppEventSender,
        has_input_focus: bool,
        enhanced_keys_supported: bool,
        disable_paste_burst

作用:按指定快捷键创建完整的用户输入弹窗。它把问题、输入框、答案状态、队列和倒计时状态都初始化好。

数据流:输入请求、事件发送器、焦点状态、粘贴设置和快捷键配置 → 创建一个纯文本 ChatComposer 输入框,关掉多余弹窗提示,初始化内部字段 → 重置当前请求状态、修正焦点、恢复当前草稿 → 输出可立即显示和操作的 overlay。

调用关系:RequestUserInputOverlay::new 会调用它;外层把新的用户输入请求推入界面时也会用它。它还会马上调用 reset_for_request、ensure_focus_available、restore_current_draft,把各个零件调到一致状态。

调用图:调用 2 个内部函数(new_with_config, plain_text);被 7 处调用(push_user_input_request, freeform_footer_shows_configured_submit_binding, freeform_submit_binding_wins_over_question_navigation, freeform_uses_configured_composer_submit_binding, request_user_input_freeform_remapped_interrupt_snapshot, request_user_input_freeform_remapped_submit_snapshot, request_user_input_uses_remapped_interrupt_binding_while_notes_are_visible);外部调用 4 个(now, new, new, clone)。

RequestUserInputOverlay::current_index240–242 ↗
fn current_index(&self) -> usize

作用:返回当前正在看的问题编号。编号从 0 开始,是内部用的坐标。

数据流:读取 self.current_idx → 原样返回这个数字 → 不修改状态。

调用关系:很多函数用它找当前题,比如 current_question、current_answer、进度提示和跳题逻辑。

调用图:被 8 处调用(current_answer, current_answer_mut, current_question, footer_tips, go_next_or_submit, notes_has_content, notes_ui_visible, progress_prefix_text)。

RequestUserInputOverlay::current_question244–246 ↗
fn current_question(&self) -> Option<&ToolRequestUserInputQuestion>

作用:拿到当前这一题的题目数据。如果没有题,就返回空。

数据流:读取当前题号 → 到请求里的 questions 列表取对应题目 → 输出题目的引用或 None。

调用关系:选项判断、选项行生成、问题换行显示等都会先通过它知道当前题是什么。

调用图:调用 1 个内部函数(current_index);被 4 处调用(has_options, option_rows, options_len, wrapped_question_lines)。

RequestUserInputOverlay::current_answer_mut248–251 ↗
fn current_answer_mut(&mut self) -> Option<&mut AnswerState>

作用:拿到当前题对应的答案状态,并且允许修改它。比如改选中项、保存草稿、标记已提交。

数据流:读取当前题号 → 到 answers 列表取可修改的 AnswerState → 返回可修改引用或 None。

调用关系:这是修改当前答案的主要入口,被保存草稿、清空选择、处理输入结果等函数频繁使用。

调用图:调用 1 个内部函数(current_index);被 12 处调用(apply_submission_draft, apply_submission_to_draft, clear_notes_and_focus_options, clear_notes_draft, clear_selection, ensure_focus_available, ensure_selected_for_notes, handle_composer_input_result, handle_key_event, handle_paste (+2 more))。

RequestUserInputOverlay::current_answer253–256 ↗
fn current_answer(&self) -> Option<&AnswerState>

作用:只读地查看当前题的答案状态。适合显示界面时用,不会改东西。

数据流:读取当前题号 → 到 answers 列表取对应 AnswerState → 返回只读引用或 None。

调用关系:恢复草稿、显示选中项、计算高度和判断备注是否可见时会用它。

调用图:调用 1 个内部函数(current_index);被 5 处调用(notes_ui_visible, options_preferred_height, options_required_height, restore_current_draft, selected_option_index)。

RequestUserInputOverlay::question_count258–260 ↗
fn question_count(&self) -> usize

作用:告诉外界这次请求一共有多少个问题。

数据流:读取 request.questions 的长度 → 返回数量 → 不修改状态。

调用关系:进度显示、题目跳转、提交判断和焦点修正都依赖它。

调用图:被 6 处调用(ensure_focus_available, footer_tips, go_next_or_submit, jump_to_question, move_question, progress_prefix_text)。

RequestUserInputOverlay::advance_queue_or_complete_at262–273 ↗
fn advance_queue_or_complete_at(&mut self, now: Instant)

作用:当前请求结束后,决定是切到下一个排队请求,还是把弹窗标记为完成。

数据流:输入当前时间 → 如果队列里有下一个请求,就取出来替换当前请求,重置开始时间和答案状态;如果没有,就把 done 设为 true → 输出为空,但会改变 overlay 状态。

调用关系:提交答案、自动空提交、服务器取消当前请求后都会调用它。它是多个请求排队时保持顺序的关键。

调用图:调用 3 个内部函数(ensure_focus_available, reset_for_request, restore_current_draft);被 3 处调用(dismiss_resolved_request, submit_answers, submit_empty_auto_resolution);外部调用 1 个(pop_front)。

RequestUserInputOverlay::snooze_auto_resolution275–279 ↗
fn snooze_auto_resolution(&mut self)

作用:用户一操作,就暂停自动提交。这样用户已经在看或在输入时,不会被倒计时突然提交掉。

数据流:检查当前请求是否启用了 auto_resolution_ms → 如果启用了,就把 auto_resolution_snoozed 设为 true → 不返回结果。

调用关系:按键和粘贴处理都会调用它,让自动处理只发生在用户完全没互动时。

调用图:被 2 处调用(handle_key_event, handle_paste)。

RequestUserInputOverlay::auto_resolution_timing_at281–301 ↗
fn auto_resolution_timing_at(&self, now: Instant) -> AutoResolutionTiming

作用:判断自动处理现在处于哪个阶段:关闭、隐藏宽限期、可见倒计时,还是已经到点。

数据流:输入当前时间 → 看请求是否启用自动处理、是否已被用户操作暂停,再计算从请求开始到现在过了多久 → 输出 AutoResolutionTiming 状态。

调用关系:倒计时文字、下一帧刷新时间、真正自动提交都基于它的判断。

调用图:被 2 处调用(auto_resolution_countdown_text_at, auto_resolution_next_frame_delay_at);外部调用 1 个(saturating_duration_since)。

RequestUserInputOverlay::auto_resolution_next_frame_delay_at303–312 ↗
fn auto_resolution_next_frame_delay_at(&self, now: Instant) -> Option<Duration>

作用:告诉界面下一次最晚多久后要重画一次,以便倒计时及时变化或自动提交。

数据流:输入当前时间 → 读取自动处理阶段 → 关闭时返回 None;隐藏期返回剩余宽限时间;倒计时时最多等 1 秒;到点时返回 0 → 不改状态。

调用关系:next_frame_delay 会调用它,外层渲染循环据此决定什么时候再唤醒。

调用图:调用 1 个内部函数(auto_resolution_timing_at);被 1 处调用(next_frame_delay);外部调用 1 个(from_secs)。

RequestUserInputOverlay::maybe_auto_resolve_at314–323 ↗
fn maybe_auto_resolve_at(&mut self, now: Instant) -> bool

作用:检查是否该自动提交空答案了,如果到点就执行。

数据流:输入当前时间 → 调 auto_resolution_timing_at 判断是否 Due → 到点就调用 submit_empty_auto_resolution,否则什么也不做 → 返回是否发生了自动提交。

调用关系:pre_draw_tick 在每次绘制前调用它,保证界面不用等用户按键也能按时处理超时。

调用图:调用 1 个内部函数(submit_empty_auto_resolution);被 1 处调用(pre_draw_tick);外部调用 1 个(matches!)。

RequestUserInputOverlay::auto_resolution_countdown_text_at325–335 ↗
fn auto_resolution_countdown_text_at(&self, now: Instant) -> Option<String>

作用:在倒计时进入可见阶段时,生成底部或界面上显示的倒计时文案。

数据流:输入当前时间 → 判断自动处理阶段 → 只有 VisibleCountdown 时才格式化剩余时间 → 输出 Some 文案或 None。

调用关系:它依赖 format_auto_resolution_remaining,把机器时间变成用户能读的提示。

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

RequestUserInputOverlay::progress_prefix_text337–351 ↗
fn progress_prefix_text(&self) -> String

作用:生成顶部或提示里的进度文字,比如“Question 2/5 (1 unanswered)”。

数据流:读取当前题号、总题数、未答题数 → 拼成进度字符串;没有题时返回“No questions” → 不改状态。

调用关系:渲染界面时用它告诉用户现在在哪一题,还有多少题没答。

调用图:调用 3 个内部函数(current_index, question_count, unanswered_count);外部调用 1 个(format!)。

RequestUserInputOverlay::has_options353–357 ↗
fn has_options(&self) -> bool

作用:判断当前题是不是选择题,也就是有没有可选项。

数据流:读取当前题 → 查看 options 是否存在且非空 → 返回 true 或 false。

调用关系:焦点、占位文字、按键行为、备注区显示等都会根据这个判断走不同路线。

调用图:调用 1 个内部函数(current_question);被 13 处调用(clear_notes_and_focus_options, clear_selection, ensure_focus_available, footer_tips, handle_composer_input_result, handle_key_event, notes_placeholder, notes_ui_visible, option_index_for_digit, options_preferred_height (+3 more))。

RequestUserInputOverlay::options_len359–363 ↗
fn options_len(&self) -> usize

作用:返回当前题一共有多少个可选项,包括可能额外加上的“Other”。

数据流:读取当前题 → 调 options_len_for_question 计算数量;没有题则是 0 → 返回数量。

调用关系:数字选项、上下移动、选中项夹紧都依赖这个数量防止越界。

调用图:调用 1 个内部函数(current_question);被 4 处调用(handle_composer_input_result, handle_key_event, option_index_for_digit, select_current_option)。

RequestUserInputOverlay::option_index_for_digit365–375 ↗
fn option_index_for_digit(&self, ch: char) -> Option<usize>

作用:把用户按下的数字键转换成选项编号,比如按 1 代表第一个选项。

数据流:输入一个字符 → 确认当前是选择题、字符是 1 到 9 且没有超过选项数量 → 返回内部选项下标或 None。

调用关系:handle_key_event 在选项区收到数字键时调用它,实现一键选择。

调用图:调用 2 个内部函数(has_options, options_len);被 1 处调用(handle_key_event)。

RequestUserInputOverlay::selected_option_index377–383 ↗
fn selected_option_index(&self) -> Option<usize>

作用:查看当前题选中了哪个选项。没有选择或不是选择题就返回空。

数据流:先确认当前题有选项 → 从当前答案状态里读取 selected_idx → 输出选项编号或 None。

调用关系:底部提示、占位文字和按键逻辑会用它判断用户是否已经选了东西。

调用图:调用 2 个内部函数(current_answer, has_options);被 3 处调用(footer_tips, handle_key_event, notes_placeholder)。

RequestUserInputOverlay::notes_has_content385–391 ↗
fn notes_has_content(&self, idx: usize) -> bool

作用:判断某一题的备注/输入框里有没有实际内容。

数据流:输入题号 → 如果是当前题,就读输入框当前文字并包含待处理粘贴;否则读保存下来的草稿文字 → 去掉空白后判断是否为空 → 返回布尔值。

调用关系:notes_ui_visible 用它决定备注区是否该继续显示,避免用户写过的备注被界面藏起来。

调用图:调用 2 个内部函数(current_text_with_pending, current_index)。

RequestUserInputOverlay::notes_ui_visible393–400 ↗
fn notes_ui_visible(&self) -> bool

作用:判断备注输入区现在该不该显示。自由输入题总是显示;选择题通常选了或写过备注才显示。

数据流:检查当前题是否有选项 → 没选项直接 true;有选项时读取当前答案的 notes_visible 或备注是否有内容 → 返回是否显示。

调用关系:焦点修正、底部提示和按键处理都会用它,避免焦点停在一个不可见输入框里。

调用图:调用 3 个内部函数(current_answer, current_index, has_options);被 3 处调用(ensure_focus_available, footer_tips, handle_key_event)。

RequestUserInputOverlay::wrapped_question_lines402–411 ↗
fn wrapped_question_lines(&self, width: u16) -> Vec<String>

作用:把当前问题文字按屏幕宽度折成多行,防止长问题挤出屏幕。

数据流:输入可用宽度 → 取当前题的 question 文本 → 用 textwrap 按宽度换行 → 输出字符串列表;没有题则输出空列表。

调用关系:渲染问题正文时会用它,让终端窗口变窄时仍然可读。

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

RequestUserInputOverlay::focus_is_notes413–415 ↗
fn focus_is_notes(&self) -> bool

作用:判断当前键盘焦点是不是在备注/输入框里。

数据流:读取 focus 字段 → 看它是否等于 Focus::Notes → 返回 true 或 false。

调用关系:底部提示、按键分发和 Ctrl+C 行为都用它判断该把按键交给输入框还是选项列表。

调用图:被 3 处调用(footer_tips, handle_key_event, on_ctrl_c);外部调用 1 个(matches!)。

RequestUserInputOverlay::confirm_unanswered_active417–419 ↗
fn confirm_unanswered_active(&self) -> bool

作用:判断“还有题没答,是否仍要提交”的确认框是否打开。

数据流:查看 confirm_unanswered 是否有值 → 返回 true 或 false。

调用关系:按键和 Ctrl+C 会先看它;如果确认框开着,按键就优先交给确认框处理。

调用图:被 2 处调用(handle_key_event, on_ctrl_c)。

RequestUserInputOverlay::option_rows421–465 ↗
fn option_rows(&self) -> Vec<GenericDisplayRow>

作用:把当前题的选项变成可显示的列表行,带编号、选中箭头和说明文字。

数据流:读取当前题和选中编号 → 遍历每个选项,生成 GenericDisplayRow;如果允许“Other”,再追加一行 Other → 输出行列表。

调用关系:选项高度计算和实际渲染都会用这些行。它把后台题目数据转换成终端列表能画的形状。

调用图:调用 1 个内部函数(current_question);被 2 处调用(options_preferred_height, options_required_height)。

RequestUserInputOverlay::options_required_height467–486 ↗
fn options_required_height(&self, width: u16) -> u16

作用:计算选项列表至少需要多少行高度才能显示当前内容。

数据流:输入屏幕宽度 → 如果不是选择题返回 0;否则生成选项行,并用当前滚动/选中状态测量换行后的高度 → 返回高度数字。

调用关系:布局计算时用它安排选项区大小,避免说明文字换行后被错误压缩。

调用图:调用 4 个内部函数(current_answer, has_options, option_rows, measure_rows_height)。

RequestUserInputOverlay::options_preferred_height488–507 ↗
fn options_preferred_height(&self, width: u16) -> u16

作用:计算选项列表理想情况下想要多少行高度。当前实现和 required 高度算法一样。

数据流:输入屏幕宽度 → 没有选项返回 0;有选项就生成行并测量显示高度 → 返回高度数字。

调用关系:界面布局可用它决定给选项区留多少空间。

调用图:调用 4 个内部函数(current_answer, has_options, option_rows, measure_rows_height)。

RequestUserInputOverlay::capture_composer_draft509–521 ↗
fn capture_composer_draft(&self) -> ComposerDraft

作用:把当前输入框里的内容打包成草稿,方便切题或提交前保存。

数据流:读取输入框当前文字、文本元素、本地图片路径和待处理粘贴 → 组装成 ComposerDraft → 返回草稿,不清空输入框。

调用关系:save_current_draft 和提交快捷键处理会用它,防止用户切题时输入丢失。

调用图:调用 4 个内部函数(current_text, local_images, pending_pastes, text_elements);被 2 处调用(handle_key_event, save_current_draft)。

RequestUserInputOverlay::save_current_draft523–535 ↗
fn save_current_draft(&mut self)

作用:保存当前题输入框里的草稿到答案状态里。

数据流:先 capture 当前输入框草稿 → 如果当前答案存在,就写入 answer.draft;如果已提交的答案被改了,就取消已提交标记;有内容时标记备注可见 → 不返回值。

调用关系:切题、提交、跳转前都会调用它,保证界面状态和内部答案状态同步。

调用图:调用 2 个内部函数(capture_composer_draft, current_answer_mut);被 5 处调用(go_next_or_submit, handle_key_event, jump_to_question, move_question, submit_answers)。

RequestUserInputOverlay::restore_current_draft537–552 ↗
fn restore_current_draft(&mut self)

作用:切到某一题后,把这题之前保存的草稿放回输入框。

数据流:根据当前题设置输入框占位文字 → 如果没有答案状态就清空输入框;否则取出草稿的文字、元素、图片和待粘贴内容放回 composer,并把光标移到末尾 → 不返回值。

调用关系:切题、换到队列里的下一个请求时调用它,让用户回到某题还能看到之前写的东西。

调用图:调用 7 个内部函数(move_cursor_to_end, set_footer_hint_override, set_pending_pastes, set_placeholder_text, set_text_content, current_answer, notes_placeholder);被 3 处调用(advance_queue_or_complete_at, jump_to_question, move_question);外部调用 2 个(new, new)。

RequestUserInputOverlay::notes_placeholder554–562 ↗
fn notes_placeholder(&self) -> &'static str

作用:决定输入框灰色提示语该写什么。不同题型和选择状态下提示不同。

数据流:检查当前题是否有选项、是否已选择 → 未选时提示先选择;已选时提示可写备注;自由输入题提示直接回答 → 返回静态字符串。

调用关系:restore_current_draft 和 sync_composer_placeholder 用它更新输入框提示。

调用图:调用 2 个内部函数(has_options, selected_option_index);被 2 处调用(restore_current_draft, sync_composer_placeholder)。

RequestUserInputOverlay::sync_composer_placeholder564–567 ↗
fn sync_composer_placeholder(&mut self)

作用:把当前应显示的占位提示同步到输入框里。

数据流:调用 notes_placeholder 得到提示文字 → 设置给 composer → 不返回值。

调用关系:选择、清空、切换焦点等状态变化后都会调用它,避免提示语和当前状态不一致。

调用图:调用 2 个内部函数(set_placeholder_text, notes_placeholder);被 7 处调用(clear_notes_and_focus_options, clear_notes_draft, clear_selection, ensure_focus_available, ensure_selected_for_notes, handle_key_event, select_current_option)。

RequestUserInputOverlay::clear_notes_draft569–580 ↗
fn clear_notes_draft(&mut self)

作用:清空当前题的备注草稿,但保留备注区域打开。

数据流:找到当前答案 → 把草稿重置、取消已提交标记、保持 notes_visible 为 true;清掉待提交草稿和输入框内容,光标移到末尾,再更新占位文字 → 不返回值。

调用关系:on_ctrl_c 在输入框里有内容时调用它,相当于第一次 Ctrl+C 先清空正在写的内容。

调用图:调用 4 个内部函数(move_cursor_to_end, set_text_content, current_answer_mut, sync_composer_placeholder);被 1 处调用(on_ctrl_c);外部调用 3 个(new, new, default)。

RequestUserInputOverlay::ensure_focus_available696–711 ↗
fn ensure_focus_available(&mut self)

作用:确保键盘焦点停在一个当前能操作的位置。比如没有选项时,焦点必须在输入框。

数据流:读取题目数量、当前题是否有选项、备注区是否可见 → 必要时把 focus 改到 Notes 或 Options,并同步占位文字 → 不返回值。

调用关系:切题、换请求、恢复草稿后调用它,防止焦点指向已经隐藏的区域。

调用图:调用 5 个内部函数(current_answer_mut, has_options, notes_ui_visible, question_count, sync_composer_placeholder);被 3 处调用(advance_queue_or_complete_at, jump_to_question, move_question);外部调用 1 个(matches!)。

RequestUserInputOverlay::reset_for_request714–743 ↗
fn reset_for_request(&mut self)

作用:为一份新的用户输入请求初始化所有答案状态。

数据流:遍历请求里的问题 → 每题创建 AnswerState;选择题默认选第一个,备注默认隐藏;自由输入题备注区默认显示 → 重置当前题号、焦点、输入框、确认框和待提交草稿。

调用关系:创建 overlay 和切到队列里的下一个请求时调用它,相当于给新问卷清场。

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

RequestUserInputOverlay::options_len_for_question745–756 ↗
fn options_len_for_question(question: &ToolRequestUserInputQuestion) -> usize

作用:计算某个问题的选项数量,并把可能的“Other”选项也算进去。

数据流:输入一个问题 → 读取 options 长度 → 如果该题启用了 Other,就加 1 → 返回数量。

调用关系:options_len 用它计算当前题数量;选项移动和数字选择都靠这个数量。

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

RequestUserInputOverlay::other_option_enabled_for_question758–764 ↗
fn other_option_enabled_for_question(question: &ToolRequestUserInputQuestion) -> bool

作用:判断某题是否应该显示额外的“Other”选项。

数据流:输入一个问题 → 检查 is_other 是否为 true,并且原本确实有选项 → 返回是否启用 Other。

调用关系:选项行生成、选项数量计算和选中标签转换都会用它保持一致。

RequestUserInputOverlay::option_label_for_index766–778 ↗
fn option_label_for_index(
        question: &ToolRequestUserInputQuestion,
        idx: usize,
    ) -> Option<String>

作用:把选项编号转换成最终提交用的选项文字。

数据流:输入问题和选项编号 → 如果编号落在普通选项内,返回对应 label;如果正好是 Other 且启用,就返回 Other 标签 → 否则返回 None。

调用关系:submit_answers 用它把内部选中下标变成发给后台的答案文字。

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

RequestUserInputOverlay::move_question781–791 ↗
fn move_question(&mut self, next: bool)

作用:切到上一题或下一题,并保存当前题草稿。

数据流:输入 next 表示方向 → 如果没有题就退出;先保存当前草稿,再用取模方式循环切换题号,恢复新题草稿并修正焦点 → 不返回值。

调用关系:go_next_or_submit 和按键处理会调用它,实现左右键、Ctrl+N、Ctrl+P 等切题。

调用图:调用 4 个内部函数(ensure_focus_available, question_count, restore_current_draft, save_current_draft);被 2 处调用(go_next_or_submit, handle_key_event)。

RequestUserInputOverlay::jump_to_question793–801 ↗
fn jump_to_question(&mut self, idx: usize)

作用:直接跳到指定题号,常用于从未答确认框回到第一道未答题。

数据流:输入目标题号 → 如果越界就不动;否则保存当前草稿,设置 current_idx,恢复目标题草稿并修正焦点 → 不返回值。

调用关系:handle_confirm_unanswered_key_event 在用户选择回去补答时调用它。

调用图:调用 4 个内部函数(ensure_focus_available, question_count, restore_current_draft, save_current_draft);被 1 处调用(handle_confirm_unanswered_key_event)。

RequestUserInputOverlay::select_current_option804–819 ↗
fn select_current_option(&mut self, committed: bool)

作用:确认当前选项选择,并可标记为已提交。

数据流:输入 committed 标记 → 如果当前题有选项,就把选中项限制在合法范围内,并设置 answer_committed → 更新输入框占位文字 → 不返回值。

调用关系:空格、回车、数字键等选择操作都会调用它。

调用图:调用 4 个内部函数(current_answer_mut, has_options, options_len, sync_composer_placeholder);被 1 处调用(handle_key_event)。

RequestUserInputOverlay::clear_selection822–837 ↗
fn clear_selection(&mut self)

作用:清掉当前选择题的选中项和备注草稿,让这题回到未选择状态。

数据流:确认当前是选择题 → 重置选项滚动状态、草稿、提交标记和备注可见性;清空输入框和待提交草稿,更新占位文字 → 不返回值。

调用关系:handle_key_event 在用户按 Backspace 或 Delete 时调用它。

调用图:调用 5 个内部函数(move_cursor_to_end, set_text_content, current_answer_mut, has_options, sync_composer_placeholder);被 1 处调用(handle_key_event);外部调用 3 个(new, new, default)。

RequestUserInputOverlay::clear_notes_and_focus_options839–854 ↗
fn clear_notes_and_focus_options(&mut self)

作用:清掉备注内容,并把焦点退回选项列表。

数据流:确认当前是选择题 → 清空答案草稿、取消提交、隐藏备注;清空输入框,把 focus 设为 Options,并更新占位文字 → 不返回值。

调用关系:Esc、Tab 或某些返回选项区的操作会调用它。

调用图:调用 5 个内部函数(move_cursor_to_end, set_text_content, current_answer_mut, has_options, sync_composer_placeholder);被 1 处调用(handle_key_event);外部调用 3 个(new, new, default)。

RequestUserInputOverlay::ensure_selected_for_notes857–862 ↗
fn ensure_selected_for_notes(&mut self)

作用:准备进入备注输入状态,确保备注区显示出来。

数据流:找到当前答案 → 把 notes_visible 设为 true → 同步输入框占位文字。

调用关系:切到备注区、粘贴文本、提交备注前都会调用它。

调用图:调用 2 个内部函数(current_answer_mut, sync_composer_placeholder);被 2 处调用(handle_key_event, handle_paste)。

RequestUserInputOverlay::go_next_or_submit865–876 ↗
fn go_next_or_submit(&mut self)

作用:用户按提交后,决定是进入下一题,还是提交整份问卷。

数据流:检查当前题是不是最后一题 → 如果不是就 move_question 到下一题;如果是最后一题,先保存草稿,再看是否有未答题,有就打开确认框,没有就 submit_answers → 不返回值。

调用关系:handle_key_event 和 handle_composer_input_result 在用户提交当前输入后调用它。

调用图:调用 7 个内部函数(current_index, move_question, open_unanswered_confirmation, question_count, save_current_draft, submit_answers, unanswered_count);被 2 处调用(handle_composer_input_result, handle_key_event)。

RequestUserInputOverlay::submit_answers879–927 ↗
fn submit_answers(&mut self)

作用:把用户填写和选择的内容整理成正式答案,发回主程序,并在历史里记录结果。

数据流:关闭未答确认框并保存草稿 → 遍历每个问题,根据已提交状态取选项标签和备注文字,组装 HashMap → 通过 app_event_tx 发送 user_input_answer,再插入历史记录 → 最后切到队列下一个请求或完成。

调用关系:go_next_or_submit 和未答确认框确认提交时调用它;它把界面状态真正变成系统事件。

调用图:调用 4 个内部函数(send, user_input_answer, advance_queue_or_complete_at, save_current_draft);被 2 处调用(go_next_or_submit, handle_confirm_unanswered_key_event);外部调用 6 个(new, new, now, new, InsertHistoryCell, format!)。

RequestUserInputOverlay::submit_empty_auto_resolution929–946 ↗
fn submit_empty_auto_resolution(&mut self, now: Instant)

作用:自动处理到期时提交一份空答案。意思是用户没有介入,就让流程继续走。

数据流:输入当前时间 → 清掉确认框,创建空答案表 → 发送 user_input_answer 和历史记录 → 调 advance_queue_or_complete_at 切换或结束。

调用关系:maybe_auto_resolve_at 到点时调用它。

调用图:调用 3 个内部函数(send, user_input_answer, advance_queue_or_complete_at);被 1 处调用(maybe_auto_resolve_at);外部调用 3 个(new, new, InsertHistoryCell)。

RequestUserInputOverlay::dismiss_resolved_request948–962 ↗
fn dismiss_resolved_request(&mut self, request: &ResolvedAppServerRequest) -> bool

作用:当外部说某个用户输入请求已经解决或取消时,把它从当前弹窗或队列里移走。

数据流:输入一个已解决请求 → 只处理 UserInput 类型;先从排队请求里删掉匹配 call_id 的项;如果当前请求也匹配,就切到下一个或完成 → 返回是否真的处理了。

调用关系:dismiss_app_server_request 会调用它,让服务器端状态变化能同步到界面。

调用图:调用 1 个内部函数(advance_queue_or_complete_at);被 1 处调用(dismiss_app_server_request);外部调用 3 个(now, len, retain)。

RequestUserInputOverlay::open_unanswered_confirmation964–968 ↗
fn open_unanswered_confirmation(&mut self)

作用:打开“还有未答题,是否仍要提交”的确认列表。

数据流:创建一个 ScrollState,并默认选中第一个选项 → 写入 confirm_unanswered → 不返回值。

调用关系:go_next_or_submit 发现还有未答题时调用它。

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

RequestUserInputOverlay::close_unanswered_confirmation970–972 ↗
fn close_unanswered_confirmation(&mut self)

作用:关闭未答题确认框。

数据流:把 confirm_unanswered 设为 None → 不返回值。

调用关系:确认框按键处理和 Ctrl+C 会调用它。

调用图:被 2 处调用(handle_confirm_unanswered_key_event, on_ctrl_c)。

RequestUserInputOverlay::unanswered_question_count974–976 ↗
fn unanswered_question_count(&self) -> usize

作用:返回当前还有几题没答。它是给确认框文案用的小包装。

数据流:调用 unanswered_count → 返回数量 → 不改状态。

调用关系:unanswered_submit_description 用它生成单复数合适的说明。

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

RequestUserInputOverlay::unanswered_submit_description978–986 ↗
fn unanswered_submit_description(&self) -> String

作用:生成未答确认框里“仍然提交”的说明文字。

数据流:读取未答数量 → 按数量选择单数或复数描述 → 拼成一句说明 → 返回字符串。

调用关系:unanswered_confirmation_rows 用它作为第一行选项的说明。

调用图:调用 1 个内部函数(unanswered_question_count);被 1 处调用(unanswered_confirmation_rows);外部调用 1 个(format!)。

RequestUserInputOverlay::first_unanswered_index988–996 ↗
fn first_unanswered_index(&self) -> Option<usize>

作用:找到第一道还没回答的问题编号。

数据流:读取当前输入框文字 → 遍历所有问题,用 is_question_answered 判断是否已答 → 返回第一个未答题号或 None。

调用关系:确认框里用户选择回去补答时,handle_confirm_unanswered_key_event 用它定位目标题。

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

RequestUserInputOverlay::unanswered_confirmation_rows998–1027 ↗
fn unanswered_confirmation_rows(&self) -> Vec<GenericDisplayRow>

作用:生成未答确认框的两行选项:仍然提交,或者回去补答。

数据流:读取确认框当前选中项 → 构造两个条目,并给选中的条目前加箭头 → 输出 GenericDisplayRow 列表。

调用关系:渲染确认框时使用这些行显示菜单。

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

RequestUserInputOverlay::is_question_answered1029–1045 ↗
fn is_question_answered(&self, idx: usize, _current_text: &str) -> bool

作用:判断指定题是否算作已经回答。

数据流:输入题号和当前文字参数 → 取对应问题和答案状态;选择题要求有选中项且已提交,自由输入题要求 answer_committed 为 true → 返回布尔值。

调用关系:unanswered_count 和 first_unanswered_index 用它统一判断未答题。

RequestUserInputOverlay::unanswered_count1048–1056 ↗
fn unanswered_count(&self) -> usize

作用:统计还有多少题没回答。

数据流:读取当前输入框文字 → 遍历所有题,用 is_question_answered 过滤未答题 → 返回数量。

调用关系:进度文字、提交前检查和确认框说明都依赖它。

调用图:调用 1 个内部函数(current_text);被 3 处调用(go_next_or_submit, progress_prefix_text, unanswered_question_count)。

RequestUserInputOverlay::notes_input_height1059–1064 ↗
fn notes_input_height(&self, width: u16) -> u16

作用:计算备注输入框应该占几行,高度不会太小也不会无限变大。

数据流:输入宽度 → 问 composer 想要多高 → 把结果限制在最小高度到最小高度加 5 行之间 → 返回高度。

调用关系:布局备注输入区时使用它。

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

RequestUserInputOverlay::apply_submission_to_draft1066–1085 ↗
fn apply_submission_to_draft(&mut self, text: String, text_elements: Vec<TextElement>)

作用:把一次提交得到的文字内容写回当前题草稿,并同步输入框显示。

数据流:输入提交文字和文本元素 → 从 composer 读取本地图片路径 → 写入当前答案的 ComposerDraft,清空 pending_pastes → 设置输入框内容、移动光标到末尾、清空 footer hint → 不返回值。

调用关系:handle_composer_input_result 在没有特殊待提交草稿时调用它。

调用图:调用 5 个内部函数(local_images, move_cursor_to_end, set_footer_hint_override, set_text_content, current_answer_mut);被 1 处调用(handle_composer_input_result);外部调用 1 个(new)。

RequestUserInputOverlay::apply_submission_draft1087–1096 ↗
fn apply_submission_draft(&mut self, draft: ComposerDraft)

作用:把已经捕获好的提交草稿直接应用到当前题和输入框。

数据流:输入 ComposerDraft → 保存到当前答案 → 把草稿文字、元素、图片和待处理粘贴恢复到 composer,移动光标并清空 footer hint → 不返回值。

调用关系:handle_composer_input_result 在 pending_submission_draft 存在时调用它,保留提交瞬间捕获的完整草稿。

调用图:调用 5 个内部函数(move_cursor_to_end, set_footer_hint_override, set_pending_pastes, set_text_content, current_answer_mut);被 1 处调用(handle_composer_input_result);外部调用 2 个(new, clone)。

RequestUserInputOverlay::handle_composer_input_result1098–1136 ↗
fn handle_composer_input_result(&mut self, result: InputResult) -> bool

作用:处理输入框返回的结果,尤其是用户按下提交后的结果。

数据流:输入 InputResult → 如果是 Submitted 或 Queued,就更新选项状态和 answer_committed,应用提交草稿或文字,然后 go_next_or_submit;其他结果不处理 → 返回是否处理了提交。

调用关系:handle_key_event 把按键交给 composer 后,会调用它判断是否该进入下一题或提交整份答案。

调用图:调用 6 个内部函数(apply_submission_draft, apply_submission_to_draft, current_answer_mut, go_next_or_submit, has_options, options_len);被 1 处调用(handle_key_event);外部调用 1 个(matches!)。

RequestUserInputOverlay::handle_confirm_unanswered_key_event1138–1178 ↗
fn handle_confirm_unanswered_key_event(&mut self, key_event: KeyEvent)

作用:处理未答确认框里的按键,比如上下移动、回车确认、Esc 返回。

数据流:输入键盘事件 → 忽略释放事件;如果确认框存在,就按键修改选中项、关闭确认框、提交答案或跳到第一道未答题 → 不返回值。

调用关系:handle_key_event 在确认框打开时会优先把所有按键交给它。

调用图:调用 4 个内部函数(close_unanswered_confirmation, first_unanswered_index, jump_to_question, submit_answers);被 1 处调用(handle_key_event);外部调用 1 个(matches!)。

RequestUserInputOverlay::prefer_esc_to_handle_key_event1182–1184 ↗
fn prefer_esc_to_handle_key_event(&self) -> bool

作用:告诉外层:Esc 键优先交给这个弹窗自己处理。

数据流:无输入 → 固定返回 true → 不改状态。

调用关系:外层按键分发会用这个偏好,避免 Esc 被别的层先吃掉。

RequestUserInputOverlay::handle_key_event1186–1422 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)

作用:这是整个弹窗的键盘总调度。用户按下的上下左右、回车、Esc、数字、Tab、Ctrl+C 类操作,大多从这里开始处理。

数据流:输入一个键盘事件 → 忽略释放事件;先暂停自动处理,再按当前状态分流:确认框、清备注、中断、提交、切题、选项移动、输入框编辑等 → 根据按键修改焦点、草稿、选中项、提交状态,必要时发送中断或提交答案。

调用关系:它是运行时最核心的入口之一。它会把文字编辑类按键交给 composer.handle_key_event,再把结果交给 handle_composer_input_result;切题交给 move_question,提交交给 go_next_or_submit。

调用图:调用 23 个内部函数(interrupt, current_text_with_pending, handle_key_event, capture_composer_draft, clear_notes_and_focus_options, clear_selection, confirm_unanswered_active, current_answer_mut, ensure_selected_for_notes, focus_is_notes (+13 more));外部调用 1 个(matches!)。

RequestUserInputOverlay::terminal_title_requires_action1424–1426 ↗
fn terminal_title_requires_action(&self) -> bool

作用:告诉外层终端标题应该标记为需要用户操作。

数据流:无输入 → 固定返回 true → 不改状态。

调用关系:外层界面可用它提醒用户当前弹窗正在等待回答。

RequestUserInputOverlay::on_ctrl_c1428–1447 ↗
fn on_ctrl_c(&mut self) -> CancellationEvent

作用:处理 Ctrl+C 取消动作。它会尽量符合用户直觉:有确认框就关并中断,有正在写的备注就先清空,否则中断请求。

数据流:读取当前是否有确认框、焦点是否在备注、输入框是否有内容 → 可能关闭确认框、清空备注,或发送 interrupt 事件并把 done 设为 true → 返回 CancellationEvent::Handled。

调用关系:外层收到 Ctrl+C 时调用它。它会用 app_event_tx.interrupt 通知主流程中断。

调用图:调用 6 个内部函数(interrupt, current_text_with_pending, clear_notes_draft, close_unanswered_confirmation, confirm_unanswered_active, focus_is_notes)。

RequestUserInputOverlay::is_complete1449–1451 ↗
fn is_complete(&self) -> bool

作用:告诉外层这个弹窗是否已经结束,可以移除了。

数据流:读取 done 字段 → 返回 true 或 false。

调用关系:外层主循环会用它判断是否还要继续显示和处理这个 overlay。

RequestUserInputOverlay::handle_paste1453–1467 ↗
fn handle_paste(&mut self, pasted: String) -> bool

作用:处理用户粘贴进来的文字。粘贴会被当成输入备注,并暂停自动处理。

数据流:输入粘贴字符串 → 空字符串直接返回 false;否则暂停自动处理,如果当前在选项区就切到备注区,显示备注并取消已提交标记 → 把文本交给 composer.handle_paste → 返回 composer 是否接收。

调用关系:外层粘贴事件会调用它;它和普通打字一样会影响答案草稿。

调用图:调用 4 个内部函数(handle_paste, current_answer_mut, ensure_selected_for_notes, snooze_auto_resolution);外部调用 1 个(matches!)。

RequestUserInputOverlay::flush_paste_burst_if_due1469–1471 ↗
fn flush_paste_burst_if_due(&mut self) -> bool

作用:在一连串快速粘贴结束时,让输入框把攒着的粘贴内容刷出来。

数据流:无显式输入 → 调用 composer.flush_paste_burst_if_due → 返回是否发生了刷新。

调用关系:外层定时检查粘贴批次时会调用它,真正逻辑交给 ChatComposer。

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

RequestUserInputOverlay::is_in_paste_burst1473–1475 ↗
fn is_in_paste_burst(&self) -> bool

作用:告诉外层当前是否正在一段连续粘贴过程中。

数据流:无显式输入 → 查询 composer.is_in_paste_burst → 返回布尔值。

调用关系:外层可以据此决定是否延迟处理或刷新粘贴内容。

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

RequestUserInputOverlay::pre_draw_tick1477–1479 ↗
fn pre_draw_tick(&mut self, now: Instant) -> bool

作用:每次绘制界面前做一次小检查,主要用于自动处理倒计时到点提交。

数据流:输入当前时间 → 调 maybe_auto_resolve_at → 返回是否因为自动处理改变了状态。

调用关系:渲染循环在画之前调用它,确保超时不会卡到下一次用户操作。

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

RequestUserInputOverlay::next_frame_delay1481–1483 ↗
fn next_frame_delay(&self) -> Option<Duration>

作用:告诉渲染循环下一次应该多久后刷新,主要服务倒计时显示。

数据流:读取当前 Instant::now → 调 auto_resolution_next_frame_delay_at → 返回可选的等待时间。

调用关系:外层主循环用它安排下一帧;没有倒计时时可以不额外唤醒。

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

RequestUserInputOverlay::try_consume_user_input_request1485–1491 ↗
fn try_consume_user_input_request(
        &mut self,
        request: ToolRequestUserInputParams,
    ) -> Option<ToolRequestUserInputParams>

作用:接收新的用户输入请求。如果当前弹窗还在处理别的请求,就把新请求排队。

数据流:输入一个 ToolRequestUserInputParams → push_back 到 queue 队尾 → 返回 None,表示请求已被这个 overlay 接收。

调用关系:外层收到后续用户输入请求时调用它;advance_queue_or_complete_at 会按先进先出顺序取出来。

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

RequestUserInputOverlay::dismiss_app_server_request1493–1495 ↗
fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool

作用:让弹窗响应服务器端请求已解决/撤销的通知。

数据流:输入 ResolvedAppServerRequest → 转交 dismiss_resolved_request → 返回是否删除或结束了相关请求。

调用关系:这是对外接口,内部具体匹配和移除逻辑由 dismiss_resolved_request 做。

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

tests::test_sender1513–1519 ↗
fn test_sender() -> (
        AppEventSender,
        tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
    )

作用:给测试创建一个假的事件发送通道,方便检查 overlay 发出了什么事件。

数据流:创建 tokio 无界通道 → 把发送端包装成 AppEventSender,保留接收端 → 返回二者。

调用关系:多数测试用它构造 RequestUserInputOverlay 并读取发出的 AppEvent。

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

tests::expect_interrupt_only1521–1531 ↗
fn expect_interrupt_only(rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>)

作用:测试辅助函数:确认事件通道里只有一个中断事件,没有别的多余事件。

数据流:输入事件接收端 → 取出一个事件并检查它是 CodexOp interrupt → 再确认通道没有额外事件 → 失败时让测试报错。

调用关系:中断相关测试用它验证 Esc 或 Ctrl+C 的行为干净可靠。

调用图:外部调用 4 个(try_recv, assert!, assert_eq!, panic!)。

tests::question_with_options1533–1555 ↗
fn question_with_options(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成一题标准的三选一测试题。

数据流:输入题目 id 和 header → 填入固定问题文字和三个选项 → 返回 ToolRequestUserInputQuestion。

调用关系:很多测试用它快速搭建带选项的请求。

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

tests::question_with_options_and_other1557–1579 ↗
fn question_with_options_and_other(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成一题带“Other”额外选项的测试题。

数据流:输入 id 和 header → 创建三个普通选项,并把 is_other 设为 true → 返回问题对象。

调用关系:用于测试 Other 选项是否显示、计数和提交正确。

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

tests::question_with_wrapped_options1581–1609 ↗
fn question_with_wrapped_options(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成带较长说明文字的选项题,用来测试换行显示。

数据流:输入 id 和 header → 创建三个带长 description 的选项 → 返回问题对象。

调用关系:快照和高度相关测试用它检查窄屏下选项说明是否排版正常。

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

tests::question_with_very_long_option_text1611–1629 ↗
fn question_with_very_long_option_text(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成含超长选项标题的测试题,用来压测界面换行和截断表现。

数据流:输入 id 和 header → 创建一个特别长的选项和一个短选项 → 返回问题对象。

调用关系:用于检查极端长文本不会把终端界面撑坏。

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

tests::question_with_long_scroll_options1631–1660 ↗
fn question_with_long_scroll_options(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成多个说明非常长的选项,用来测试滚动和可见性。

数据流:输入 id 和 header → 创建四个选项,其中前三个说明很长 → 返回问题对象。

调用关系:滚动列表相关测试用它确认选中项移动后仍能保持可见。

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

tests::question_without_options1662–1671 ↗
fn question_without_options(id: &str, header: &str) -> ToolRequestUserInputQuestion

作用:生成一题没有选项的自由输入测试题。

数据流:输入 id 和 header → 创建 options 为 None 的问题 → 返回问题对象。

调用关系:自由输入题的提交、快捷键和占位文字测试会用它。

tests::request_event1673–1684 ↗
fn request_event(
        turn_id: &str,
        questions: Vec<ToolRequestUserInputQuestion>,
    ) -> ToolRequestUserInputParams

作用:把一组测试问题包装成一次用户输入请求。

数据流:输入 turn_id 和问题列表 → 填入固定 thread_id、item_id,并默认关闭自动处理 → 返回 ToolRequestUserInputParams。

调用关系:测试创建 overlay 时通常先用它构造请求。

tests::request_event_with_auto_resolution1686–1693 ↗
fn request_event_with_auto_resolution(
        turn_id: &str,
        questions: Vec<ToolRequestUserInputQuestion>,
    ) -> ToolRequestUserInputParams

作用:创建一份启用自动处理的测试请求。

数据流:输入 turn_id 和问题列表 → 先调用 request_event 创建普通请求,再把 auto_resolution_ms 设为 Some(60000) → 返回请求。

调用关系:自动倒计时和自动空提交测试用它。

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

tests::snapshot_buffer1695–1705 ↗
fn snapshot_buffer(buf: &Buffer) -> String

作用:把测试渲染出来的 Buffer 转成普通字符串,方便做快照对比。

数据流:输入终端缓冲区 → 逐行逐列读取每个格子的显示字符 → 用换行拼起来 → 返回字符串。

调用关系:render_snapshot_at 调用它,把界面绘制结果变成可断言文本。

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

tests::render_snapshot1707–1709 ↗
fn render_snapshot(overlay: &RequestUserInputOverlay, area: Rect) -> String

作用:用当前时间渲染 overlay,并返回文本快照。

数据流:输入 overlay 和区域大小 → 取 Instant::now → 调 render_snapshot_at → 返回字符串快照。

调用关系:不关心具体时间的测试用它快速获取界面样子。

调用图:外部调用 2 个(now, render_snapshot_at)。

tests::render_snapshot_at1711–1715 ↗
fn render_snapshot_at(overlay: &RequestUserInputOverlay, area: Rect, now: Instant) -> String

作用:在指定时间点渲染 overlay,并返回文本快照。

数据流:输入 overlay、区域和时间 → 创建空 Buffer,调用 overlay.render_ui_at 绘制,再 snapshot_buffer 转成字符串 → 返回快照。

调用关系:倒计时测试需要指定时间,所以会用它控制渲染时刻。

调用图:外部调用 3 个(empty, render_ui_at, snapshot_buffer)。

tests::queued_requests_are_fifo1718–1741 ↗
fn queued_requests_are_fifo()

作用:测试多个用户输入请求排队时是否按先进先出的顺序处理。

数据流:创建 overlay 和两个排队请求 → 连续 submit_answers → 检查当前 request.turn_id 先变成 turn-2,再变成 turn-3。

调用关系:验证 try_consume_user_input_request 和 advance_queue_or_complete_at 配合正确。

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

tests::interrupt_discards_queued_requests_and_emits_interrupt1744–1772 ↗
fn interrupt_discards_queued_requests_and_emits_interrupt()

作用:测试中断当前弹窗时,会结束 overlay,并且只发出中断事件。

数据流:创建当前请求和两个排队请求 → 模拟按 Esc → 检查 done 为 true,并用 expect_interrupt_only 验证事件。

调用关系:覆盖 handle_key_event 的中断路径,确保排队请求不会继续冒出来。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, expect_interrupt_only, request_event, test_sender, vec!)。

tests::auto_resolution_absent_has_no_timer1775–1792 ↗
fn auto_resolution_absent_has_no_timer()

作用:测试没有启用自动处理时,不应该有倒计时、刷新延迟或提示文字。

数据流:创建普通请求 overlay → 在当前时间检查 auto_resolution_timing_at、next_frame_delay 和 countdown_text → 断言分别是 Disabled、None、None。

调用关系:验证 auto_resolution_timing_at 等函数在默认请求下不会误触发。

调用图:调用 1 个内部函数(new);外部调用 5 个(now, assert_eq!, request_event, test_sender, vec!)。

tests::auto_resolution_hides_timer_during_grace_period1795–1823 ↗
fn auto_resolution_hides_timer_during_grace_period()

作用:测试自动处理刚开始的宽限期内,倒计时存在但不显示给用户。

数据流:创建启用自动处理的 overlay,并把开始时间设为 now → 检查 timing 是 HiddenGrace、下一帧延迟是宽限期剩余时间,再渲染快照确认没有 auto-resolves 文案。

调用关系:验证 auto_resolution_timing_at、auto_resolution_next_frame_delay_at 和渲染显示之间的配合。

调用图:调用 1 个内部函数(new);外部调用 6 个(now, assert!, assert_eq!, request_event_with_auto_resolution, test_sender, vec!)。

tests::auto_resolution_visible_countdown_snapshot1826–1849 ↗
fn auto_resolution_visible_countdown_snapshot()

作用:检查自动提交进入可见倒计时阶段时,界面长什么样。它用快照测试防止倒计时提示被意外改坏。

数据流:进去的是一个带自动解决时间的三题请求和当前时间;测试把请求开始时间往前调到倒计时刚该显示的时候;出来的是一张渲染快照,用来和保存的标准画面对比。

调用关系:测试框架运行它;它先创建 RequestUserInputOverlay,再调用渲染辅助函数,最后交给 insta 快照工具判断界面是否和预期一致。

调用图:调用 1 个内部函数(new);外部调用 5 个(now, assert_snapshot!, request_event_with_auto_resolution, test_sender, vec!)。

tests::auto_resolution_visible_countdown_is_red1852–1889 ↗
fn auto_resolution_visible_countdown_is_red()

作用:确认“auto-resolves in ...”这段倒计时文字是红色的。这样用户能明显看出时间快到了。

数据流:进去的是一个带自动解决的请求、一个画布和当前时间;测试让弹窗渲染到画布里,再找到倒计时文字所在位置;出来的是颜色断言:倒计时每个字符都是红色,而普通的“Question”前缀不是红色。

调用关系:测试框架调用它;它通过 render_ui_at 走真实绘制路径,再读取 Buffer 里的颜色信息,验证显示层没有把警告样式弄丢。

调用图:调用 1 个内部函数(new);外部调用 9 个(empty, now, new, assert_eq!, assert_ne!, request_event_with_auto_resolution, snapshot_buffer, test_sender, vec!)。

tests::auto_resolution_expiry_emits_empty_answer1892–1923 ↗
fn auto_resolution_expiry_emits_empty_answer()

作用:检查自动解决时间到期后,弹窗会自动提交空答案。这样程序不会永远等用户输入。

数据流:进去的是一个带自动解决的请求和事件接收器;测试把开始时间调到已经超时,再触发绘制前检查;出来的是弹窗标记完成,并发出 UserInputAnswer 事件,答案表为空,随后还发出一条历史记录事件。

调用关系:测试框架运行它;它主要验证 pre_draw_tick 在超时时会调用提交流程,并通过事件通道把结果交给应用其他部分。

调用图:调用 1 个内部函数(new);外部调用 7 个(now, assert!, assert_eq!, panic!, request_event_with_auto_resolution, test_sender, vec!)。

tests::auto_resolution_key_interaction_snoozes_timer1926–1950 ↗
fn auto_resolution_key_interaction_snoozes_timer()

作用:确认用户按了键以后,自动解决计时会被暂停。因为用户已经在操作,就不应该再突然自动提交空答案。

数据流:进去的是一个快到自动解决阶段的请求和一个向下键;测试把按键送进弹窗;出来的是自动解决状态变成 Disabled,之后即使时间超过原本期限,也不会提交事件。

调用关系:测试框架调用它;它通过 handle_key_event 模拟真实用户操作,再用 auto_resolution_timing_at 和 pre_draw_tick 检查计时器是否被正确关掉。

调用图:调用 1 个内部函数(new);外部调用 7 个(now, from, assert!, assert_eq!, request_event_with_auto_resolution, test_sender, vec!)。

tests::auto_resolution_paste_interaction_snoozes_timer1953–1977 ↗
fn auto_resolution_paste_interaction_snoozes_timer()

作用:确认用户粘贴内容以后,也会暂停自动解决计时。粘贴同样说明用户正在回应。

数据流:进去的是一个带自动解决的请求和粘贴文本;测试调用 handle_paste;出来的是自动解决状态关闭,后续超时检查不会发出答案事件。

调用关系:测试框架运行它;它验证粘贴入口和按键入口一样,会通知弹窗“用户已经互动过”,从而避免自动空提交。

调用图:调用 1 个内部函数(new);外部调用 6 个(now, assert!, assert_eq!, request_event_with_auto_resolution, test_sender, vec!)。

tests::auto_resolution_resets_for_queued_request1980–2012 ↗
fn auto_resolution_resets_for_queued_request()

作用:检查当前请求自动结束后,如果队列里还有下一个请求,计时器会为新请求重新开始。否则新问题可能一出现就被旧计时器误判超时。

数据流:进去的是两个带自动解决的请求;测试先把第二个排队,再让第一个超时;出来的是事件通道收到第一个请求的结果,弹窗切到第二个请求,并且第二个请求的自动解决回到隐藏宽限期。

调用关系:测试框架调用它;它通过 try_consume_user_input_request 模拟排队,再让 pre_draw_tick 驱动当前请求结束,验证切换请求时状态被重置。

调用图:调用 1 个内部函数(new);外部调用 6 个(now, assert!, assert_eq!, request_event_with_auto_resolution, test_sender, vec!)。

tests::resolved_request_dismisses_overlay_without_emitting_events2015–2041 ↗
fn resolved_request_dismisses_overlay_without_emitting_events()

作用:确认服务端说当前请求已经解决时,弹窗会直接关闭,但不会再多发答案或中断事件。这样可以避免重复上报。

数据流:进去的是一个请求和一个匹配 call_id 的服务端解决通知;测试调用 dismiss_app_server_request;出来的是弹窗 done 为真,事件接收器为空。

调用关系:测试框架运行它;它验证服务端外部解决请求时,弹窗只做本地收起,不把这件事误当成用户提交或取消。

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

tests::resolved_current_request_advances_to_next_same_turn_prompt2044–2081 ↗
fn resolved_current_request_advances_to_next_same_turn_prompt()

作用:检查当前请求被服务端解决后,如果同一轮对话里还有更新的问题,弹窗会切到下一个问题而不是彻底关闭。

数据流:进去的是当前请求、一个排队请求,以及当前请求的解决通知;测试调用 dismiss_app_server_request;出来的是弹窗仍未结束,当前请求变成 call-2,且没有发出事件。

调用关系:测试框架调用它;它验证 dismiss_app_server_request 会和内部请求队列配合,只移除已解决的那个请求。

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

tests::resolved_queued_request_removes_only_that_prompt2084–2136 ↗
fn resolved_queued_request_removes_only_that_prompt()

作用:确认服务端解决的是排队中的请求时,只删除那一个,不影响正在显示的请求和其他排队请求。

数据流:进去的是三个同轮请求和一个针对第二个请求的解决通知;测试移除第二个后提交当前第一个;出来的是弹窗从第一个切到第三个,并且提交第一个时正常发出答案和历史事件。

调用关系:测试框架运行它;它把 dismiss_app_server_request、submit_answers 和队列推进连起来验证,防止队列顺序被破坏。

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

tests::options_can_submit_empty_when_unanswered2139–2158 ↗
fn options_can_submit_empty_when_unanswered()

作用:确认选择题即使没选答案,也可以被直接提交为空。这样自动提交或强制提交时不会因为空选择崩掉。

数据流:进去的是一个选择题请求;测试直接调用 submit_answers;出来的是 UserInputAnswer 事件,其中 q1 的 answers 是空列表。

调用关系:测试框架调用它;它验证 submit_answers 的打包逻辑能表达“这题没有回答”,而不是漏掉字段或报错。

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

tests::enter_commits_default_selection_on_last_option_question2161–2179 ↗
fn enter_commits_default_selection_on_last_option_question()

作用:确认在最后一个选择题上按回车,会提交默认高亮的第一个选项。用户不需要先手动移动光标也能快速确认。

数据流:进去的是一个选择题请求和回车键;测试把回车送给弹窗;出来的是答案事件,q1 的答案为 Option 1。

调用关系:测试框架运行它;它验证 handle_key_event 对最后一题的回车会走完整提交流程。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::enter_commits_default_selection_on_non_last_option_question2182–2227 ↗
fn enter_commits_default_selection_on_non_last_option_question()

作用:确认不是最后一题时按回车,只确认当前题并跳到下一题,不会提前提交整份答案。

数据流:进去的是两个选择题和两次回车;第一次回车后,当前题变成已确认并切到第二题,没有事件;第二次回车后,出来的是包含两题默认选项的提交事件。

调用关系:测试框架调用它;它验证 handle_key_event、当前题索引和 submit_answers 之间的分工:先逐题确认,最后一题才整体发送。

调用图:调用 1 个内部函数(new);外部调用 8 个(new, from, assert!, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::number_keys_select_and_submit_options2230–2248 ↗
fn number_keys_select_and_submit_options()

作用:确认按数字键可以直接选择并提交对应选项。这样选择题操作更快。

数据流:进去的是一个选择题和数字键 2;测试送入按键;出来的是答案事件,答案为 Option 2。

调用关系:测试框架运行它;它验证 handle_key_event 能把数字字符解释成选项编号,并在单题场景下直接提交。

调用图:调用 1 个内部函数(new);外部调用 7 个(Char, from, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::vim_keys_move_option_selection2251–2270 ↗
fn vim_keys_move_option_selection()

作用:确认 Vim 风格按键 j/k 可以上下移动选项。Vim 风格指的是一些程序员常用的键盘导航习惯。

数据流:进去的是一个选择题,初始选中第一个;测试依次按 j 和 k;出来的是选中项先变成第二个,再回到第一个。

调用关系:测试框架调用它;它验证 handle_key_event 对选项焦点下的 j/k 分支会改变 options_state.selected_idx。

调用图:调用 1 个内部函数(new);外部调用 6 个(Char, from, assert_eq!, request_event, test_sender, vec!)。

tests::typing_in_options_does_not_open_notes2273–2296 ↗
fn typing_in_options_does_not_open_notes()

作用:确认在选择题选项区随便打字,不会误打开备注输入框。这样用户按错字母不会把界面状态弄乱。

数据流:进去的是两个选择题和字符 x;测试送入按键;出来的是当前题仍是第一题,备注框不可见,焦点仍在选项区,输入框内容为空。

调用关系:测试框架运行它;它验证 handle_key_event 只接受有意义的选项快捷键,不把普通字符转交给备注编辑器。

调用图:调用 1 个内部函数(new);外部调用 7 个(Char, from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::h_l_move_between_questions_in_options2299–2320 ↗
fn h_l_move_between_questions_in_options()

作用:确认在选项区可以用 h/l 在问题之间左右切换。这里 h/l 是 Vim 风格的左右键。

数据流:进去的是两个选择题;测试按 l 后当前题从 0 到 1,再按 h 回到 0;出来的是当前索引变化符合预期。

调用关系:测试框架调用它;它验证按键处理会调用问题切换逻辑,而不是只在当前题内移动选项。

调用图:调用 1 个内部函数(new);外部调用 6 个(Char, from, assert_eq!, request_event, test_sender, vec!)。

tests::left_right_move_between_questions_in_options2323–2344 ↗
fn left_right_move_between_questions_in_options()

作用:确认普通左右方向键也能在多个问题之间切换。这样不熟悉 Vim 的用户也能操作。

数据流:进去的是两个选择题和右、左方向键;测试送入按键;出来的是当前题索引先到第二题,再回第一题。

调用关系:测试框架运行它;它验证 handle_key_event 的方向键分支会驱动 move_question。

调用图:调用 1 个内部函数(new);外部调用 5 个(from, assert_eq!, request_event, test_sender, vec!)。

tests::horizontal_list_keys_move_between_questions_in_options2347–2368 ↗
fn horizontal_list_keys_move_between_questions_in_options()

作用:确认 Ctrl+h 和 Ctrl+l 也能左右切换问题。Ctrl 是组合键,用来兼容横向列表的快捷键习惯。

数据流:进去的是两个选择题和两个组合键;测试按 Ctrl+l 再 Ctrl+h;出来的是当前题索引先前进再后退。

调用关系:测试框架调用它;它验证增强的横向导航快捷键和普通左右切题走同一套状态更新。

调用图:调用 1 个内部函数(new);外部调用 6 个(Char, new, assert_eq!, request_event, test_sender, vec!)。

tests::options_notes_focus_hides_question_navigation_tip2371–2405 ↗
fn options_notes_focus_hides_question_navigation_tip()

作用:确认打开选项备注框后,页脚不再显示左右切题提示。因为这时用户焦点在写备注,提示应该换成清除备注相关内容。

数据流:进去的是两个选择题;测试先读取默认页脚提示,再按 Tab 打开备注;出来的是提示列表从“添加备注、提交、切题、中断”变成“清除备注、提交”。

调用关系:测试框架运行它;它验证 footer_tips 会根据当前焦点 Focus 动态变化,而不是固定显示一套提示。

调用图:调用 1 个内部函数(new);外部调用 5 个(from, assert_eq!, request_event, test_sender, vec!)。

tests::freeform_shows_ctrl_p_and_ctrl_n_question_navigation_tip2408–2435 ↗
fn freeform_shows_ctrl_p_and_ctrl_n_question_navigation_tip()

作用:确认自由填写题的页脚会提示用 Ctrl+p / Ctrl+n 切换问题。自由填写题里方向键通常用于编辑文本,所以切题要用更明确的组合键。

数据流:进去的是一个选择题加一个自由填写题;测试切到自由填写题后读取页脚;出来的是提示包含提交全部、Ctrl+p/Ctrl+n 改题、Esc 中断。

调用关系:测试框架调用它;它验证 move_question 切换题型后,footer_tips 会按自由文本编辑场景更新说明。

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

tests::request_user_input_uses_remapped_interrupt_binding_while_notes_are_visible2460–2491 ↗
fn request_user_input_uses_remapped_interrupt_binding_while_notes_are_visible()

作用:确认备注框打开时,中断快捷键也会按用户配置生效。这里把中断键改成 F12。

数据流:进去的是一个选择题和改过的键位配置;测试打开备注框,检查页脚显示 f12 to interrupt,再按 F12;出来的是弹窗结束,事件通道只收到中断事件。

调用关系:测试框架调用它;它验证 new_with_keymap、footer_tips 和 handle_key_event 对同一份快捷键配置保持一致。

调用图:调用 2 个内部函数(new_with_keymap, defaults);外部调用 7 个(F, from, assert_eq!, expect_interrupt_only, request_event, test_sender, vec!)。

tests::tab_opens_notes_when_option_selected2494–2510 ↗
fn tab_opens_notes_when_option_selected()

作用:确认选择了某个选项后按 Tab 会打开备注输入区。用户可以给选项补充说明。

数据流:进去的是一个选择题,测试先手动选中第二个选项;按 Tab 后;出来的是备注界面可见,焦点变成 Notes。

调用关系:测试框架运行它;它验证 handle_key_event 在已有选项选择时会切换到备注编辑模式。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::switching_to_options_resets_notes_focus_when_notes_hidden2513–2534 ↗
fn switching_to_options_resets_notes_focus_when_notes_hidden()

作用:确认从自由填写题切到选择题时,如果没有显示备注框,焦点会回到选项区。否则用户可能看着选项却还在不可见的输入框里打字。

数据流:进去的是一个自由填写题和一个选择题;测试从第一题移动到第二题;出来的是焦点为 Options,备注界面不可见。

调用关系:测试框架调用它;它验证 move_question 在题型变化时会重置焦点,避免旧题的编辑状态污染新题。

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

tests::switching_from_freeform_with_text_resets_focus_and_keeps_last_option_empty2537–2580 ↗
fn switching_from_freeform_with_text_resets_focus_and_keeps_last_option_empty()

作用:确认从带草稿的自由填写题切到选择题时,那段未确认草稿不会被当成答案提交。

数据流:进去的是自由填写题、选择题和一段未按回车确认的文字;测试切到选择题,先触发未回答确认,再选择选项并提交;出来的是自由填写题答案为空,选择题答案为 Option 1。

调用关系:测试框架运行它;它验证 move_question、确认未回答提示和 submit_answers 一起保护“草稿不等于已提交答案”的规则。

调用图:调用 1 个内部函数(new);外部调用 9 个(Char, from, new, assert!, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::esc_in_notes_mode_without_options_interrupts2583–2597 ↗
fn esc_in_notes_mode_without_options_interrupts()

作用:确认纯自由填写题里按 Esc 会中断请求。因为没有选项备注这种可关闭的小面板,Esc 就是退出。

数据流:进去的是一个自由填写题和 Esc 键;测试送入按键;出来的是弹窗结束,事件通道只收到中断事件。

调用关系:测试框架调用它;它验证 handle_key_event 在 Notes 焦点且没有选项背景时,会走 interrupt 流程。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert_eq!, expect_interrupt_only, request_event, test_sender, vec!)。

tests::esc_in_options_mode_interrupts2600–2614 ↗
fn esc_in_options_mode_interrupts()

作用:确认选择题选项区按 Esc 会中断请求。用户可以随时取消本次问答。

数据流:进去的是一个选择题和 Esc 键;测试送入按键;出来的是弹窗结束,并只发出中断事件。

调用关系:测试框架运行它;它验证选项焦点下的 Esc 不会被误当成清除备注,而是交给中断流程。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert_eq!, expect_interrupt_only, request_event, test_sender, vec!)。

tests::esc_in_notes_mode_clears_notes_and_hides_ui2617–2642 ↗
fn esc_in_notes_mode_clears_notes_and_hides_ui()

作用:确认选择题的备注模式里按 Esc 只是清掉备注并关掉备注框,不会中断整个请求。

数据流:进去的是一个已选中并打开备注框的选择题;测试按 Esc;出来的是弹窗仍打开,焦点回选项区,备注文本和草稿清空,选项仍保留,且没有事件发出。

调用关系:测试框架调用它;它验证同一个 Esc 在不同焦点下行为不同:备注模式优先关闭备注小面板。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::esc_in_notes_mode_with_text_clears_notes_and_hides_ui2645–2671 ↗
fn esc_in_notes_mode_with_text_clears_notes_and_hides_ui()

作用:确认备注框里已经输入文字时按 Esc,也会清空文字并关闭备注框。它不会把半截备注偷偷保存。

数据流:进去的是一个已选选项、已打开备注框并输入 a 的状态;测试按 Esc;出来的是备注界面关闭,输入框和草稿为空,选项还在,没有事件。

调用关系:测试框架运行它;它验证备注编辑器里的临时文本会被清理,状态回到选项选择。

调用图:调用 1 个内部函数(new);外部调用 7 个(Char, from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::esc_drops_committed_answers2674–2699 ↗
fn esc_drops_committed_answers()

作用:确认即使前面题目已经确认过,之后按 Esc 中断时也不会发送那些已确认答案。中断就是放弃整次请求。

数据流:进去的是一个选择题和一个自由填写题;测试先回车确认第一题但未完成全部,再按 Esc;出来的是事件通道只收到中断,没有答案提交。

调用关系:测试框架调用它;它验证 handle_key_event 的中断流程优先级高于已暂存答案,防止半份答案被误发。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, expect_interrupt_only, request_event, test_sender, vec!)。

tests::backspace_in_options_clears_selection2702–2720 ↗
fn backspace_in_options_clears_selection()

作用:确认在选项区按退格键会清除当前选择。用户可以撤销刚才高亮或选中的选项。

数据流:进去的是一个选择题,当前选中第二项;测试按 Backspace;出来的是 selected_idx 变成 None,备注界面不显示,也没有事件。

调用关系:测试框架运行它;它验证 handle_key_event 对 Backspace 的选项区行为是本地清除选择。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::backspace_on_empty_notes_closes_notes_ui2723–2746 ↗
fn backspace_on_empty_notes_closes_notes_ui()

作用:确认备注框为空时按退格键,会关闭备注框并回到选项区。类似手机输入框空了再退一步就退出编辑。

数据流:进去的是一个已选选项并打开空备注框的状态;测试按 Backspace;出来的是焦点回 Options,备注不可见,选项保持不变,没有事件。

调用关系:测试框架调用它;它验证备注编辑模式下 Backspace 会先看文本是否为空,再决定关闭备注界面。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::tab_in_notes_clears_notes_and_hides_ui2749–2775 ↗
fn tab_in_notes_clears_notes_and_hides_ui()

作用:确认在备注框里按 Tab 会清除备注并关闭备注框。页脚提示里的“tab 清除备注”必须真的生效。

数据流:进去的是一个已选选项、打开备注并写入 Some notes 的状态;测试再按 Tab;出来的是焦点回选项区,备注和草稿都清空,选项保留,没有事件。

调用关系:测试框架运行它;它验证 handle_key_event 中 Tab 在 Notes 焦点下不是打开备注,而是关闭并清理备注。

调用图:调用 1 个内部函数(new);外部调用 7 个(from, new, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::skipped_option_questions_count_as_unanswered2778–2789 ↗
fn skipped_option_questions_count_as_unanswered()

作用:确认完全跳过的选择题会被统计为未回答。这样提交前的提醒数量才准确。

数据流:进去的是一个未操作的选择题;测试调用 unanswered_count;出来的是数量 1。

调用关系:测试框架调用它;它验证未回答统计会把没有选择的选项题算进去。

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

tests::highlighted_option_questions_are_unanswered2792–2805 ↗
fn highlighted_option_questions_are_unanswered()

作用:确认只是高亮某个选项,还不算真正回答。用户必须确认才算数。

数据流:进去的是一个选择题,测试把 selected_idx 设为第一个;调用 unanswered_count;出来的未回答数仍是 1。

调用关系:测试框架运行它;它验证 selected_idx 和 answer_committed 的含义不同,避免光标停在哪就被当成答案。

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

tests::freeform_requires_enter_with_text_to_mark_answered2808–2834 ↗
fn freeform_requires_enter_with_text_to_mark_answered()

作用:确认自由填写题只有在有文字并按回车后,才标记为已回答。

数据流:进去的是两个自由填写题和一段 Draft;测试先看未回答数为 2,再按回车;出来的是第一题 answer_committed 为真,未回答数变成 1。

调用关系:测试框架调用它;它验证 composer 里的草稿必须通过 handle_key_event 的提交动作才会进入答案状态。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, new, assert_eq!, request_event, test_sender, vec!)。

tests::freeform_enter_with_empty_text_is_unanswered2837–2857 ↗
fn freeform_enter_with_empty_text_is_unanswered()

作用:确认自由填写题空着按回车,不会被算作已回答。空提交和已回答是两回事。

数据流:进去的是两个自由填写题和一次回车;因为输入框为空;出来的是第一题仍未确认,未回答数仍为 2。

调用关系:测试框架运行它;它验证自由文本提交逻辑不会把空内容误标为已完成。

调用图:调用 1 个内部函数(new);外部调用 5 个(from, assert_eq!, request_event, test_sender, vec!)。

tests::freeform_shift_enter_inserts_newline_without_advancing2860–2886 ↗
fn freeform_shift_enter_inserts_newline_without_advancing()

作用:确认支持增强按键时,Shift+Enter 会在自由填写题里插入换行,而不是跳到下一题。

数据流:进去的是两个自由填写题、文本 Draft 和 Shift+Enter;测试执行后;出来的是当前题仍为第一题,文本变成 Draft 加换行,答案未确认。

调用关系:测试框架调用它;它验证 composer 编辑器优先处理换行编辑动作,不触发题目提交。

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

tests::freeform_uses_configured_composer_submit_binding2889–2917 ↗
fn freeform_uses_configured_composer_submit_binding()

作用:确认自由填写题会使用配置里的提交快捷键。这里提交键被改成 Ctrl+j。

数据流:进去的是自定义键位、两个自由填写题和 Draft 文本;测试按 Ctrl+j;出来的是第一题被确认,当前题切到第二题。

调用关系:测试框架运行它;它通过 new_with_keymap 验证 handle_key_event 会读取 RuntimeKeymap 的 composer.submit 配置。

调用图:调用 2 个内部函数(new_with_keymap, defaults);外部调用 7 个(Char, new, new, assert_eq!, request_event, test_sender, vec!)。

tests::freeform_submit_binding_wins_over_question_navigation2920–2948 ↗
fn freeform_submit_binding_wins_over_question_navigation()

作用:确认如果提交快捷键和切题快捷键冲突,提交优先。这样用户自定义的提交键不会被导航抢走。

数据流:进去的是把提交键设为 Ctrl+n 的配置、两个自由填写题和 Draft;测试按 Ctrl+n;出来的是第一题确认并前进到第二题。

调用关系:测试框架调用它;它验证按键匹配顺序:配置的提交动作先于默认问题导航动作。

调用图:调用 2 个内部函数(new_with_keymap, defaults);外部调用 7 个(Char, new, new, assert_eq!, request_event, test_sender, vec!)。

tests::freeform_questions_submit_empty_when_empty2951–2969 ↗
fn freeform_questions_submit_empty_when_empty()

作用:确认自由填写题为空时,直接整体提交会发送空答案。这样协议里能明确表达“用户没填”。

数据流:进去的是一个自由填写题;测试直接 submit_answers;出来的是答案事件,q1 的 answers 为空列表。

调用关系:测试框架运行它;它验证 submit_answers 能为自由文本题生成空答案结构。

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

tests::freeform_draft_is_not_submitted_without_enter2972–2994 ↗
fn freeform_draft_is_not_submitted_without_enter()

作用:确认自由填写题里没按回车确认的草稿不会被整体提交。草稿只是正在编辑的内容,不是正式答案。

数据流:进去的是一个自由填写题和 Draft text 草稿;测试直接 submit_answers;出来的是提交事件中 q1 答案为空。

调用关系:测试框架调用它;它验证 submit_answers 只收集已确认答案,不偷拿 composer 当前草稿。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::freeform_commit_resets_when_draft_changes2997–3037 ↗
fn freeform_commit_resets_when_draft_changes()

作用:确认自由填写题已确认后,如果回去修改草稿,已确认状态会被取消。否则旧答案可能和新草稿不一致。

数据流:进去的是两个自由填写题;测试先提交 Committed,再回到第一题改成 Edited,然后切走并整体提交;出来的是 q1 答案为空,表示修改后未重新确认。

调用关系:测试框架运行它;它验证捕获草稿、移动问题和提交答案之间会维护“修改后需重新确认”的规则。

调用图:调用 1 个内部函数(new);外部调用 7 个(from, new, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::notes_are_captured_for_selected_option3040–3079 ↗
fn notes_are_captured_for_selected_option()

作用:确认给已选选项写的备注会一起提交。答案里既有选项,也有 user_note 开头的用户备注。

数据流:进去的是一个选择题,测试选中 Option 2 并写入 Notes for option 2;提交后;出来的是 q1 答案列表包含 Option 2 和 user_note: Notes for option 2。

调用关系:测试框架调用它;它验证 select_current_option、capture_composer_draft 和 submit_answers 会把选项与备注合并成协议需要的答案。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::notes_submission_commits_selected_option3082–3111 ↗
fn notes_submission_commits_selected_option()

作用:确认在备注框里按回车提交时,会同时确认当前选中的选项。用户写完备注不需要再回去单独确认选项。

数据流:进去的是两个选择题;测试先选第二个选项,打开备注,写 Notes,再按回车;出来的是当前题切到第二题,第一题选中第二项且已确认。

调用关系:测试框架运行它;它验证备注提交路径和普通选项提交路径会汇合,都会设置 answer_committed。

调用图:调用 1 个内部函数(new);外部调用 7 个(from, new, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::is_other_adds_none_of_the_above_and_submits_it3114–3164 ↗
fn is_other_adds_none_of_the_above_and_submits_it()

作用:确认带“其他”能力的选择题会显示“None of the above”,并能提交它和自定义备注。

数据流:进去的是一个带 other 选项的选择题;测试检查最后一行是 None of the above,再选它并写 Custom answer;出来的是答案包含 OTHER_OPTION_LABEL 和 user_note: Custom answer。

调用关系:测试框架调用它;它验证 option_rows 会补上特殊选项,submit_answers 会把特殊选项和备注按同一格式发出。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, assert_eq!, panic!, request_event, test_sender, vec!)。

tests::large_paste_is_preserved_when_switching_questions3167–3192 ↗
fn large_paste_is_preserved_when_switching_questions()

作用:确认大段粘贴内容在切换问题时不会丢。大粘贴会用占位符暂存,避免界面卡顿或文本太长难处理。

数据流:进去的是两个自由填写题和 1500 个 x 的粘贴文本;测试粘贴后切到下一题;出来的是第一题草稿里保留一个 pending_paste,占位符仍在文本中,展开后的文本等于原始大粘贴。

调用关系:测试框架运行它;它验证 composer 的粘贴缓存和 move_question 的草稿保存能正确配合。

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

tests::pending_paste_placeholder_survives_submission_and_back_navigation3195–3223 ↗
fn pending_paste_placeholder_survives_submission_and_back_navigation()

作用:确认选项备注里的大粘贴,在提交当前题后再返回,也仍能恢复。占位符不能因为切题或提交而损坏。

数据流:进去的是两个选择题和 1200 个 x 的粘贴文本;测试在备注模式粘贴,按回车提交,再用 Ctrl+p 回到上一题;出来的是第一题草稿仍有 pending_paste,占位符存在,展开文本等于原粘贴。

调用关系:测试框架调用它;它验证备注提交、题目导航和粘贴占位保存三件事连续发生时仍然安全。

调用图:调用 1 个内部函数(new);外部调用 8 个(Char, from, new, assert!, assert_eq!, request_event, test_sender, vec!)。

tests::request_user_input_options_snapshot3226–3240 ↗
fn request_user_input_options_snapshot()

作用:保存普通选择题弹窗的标准画面。以后改界面时可以立刻发现外观变化。

数据流:进去的是一个选择题和固定大小区域;测试渲染弹窗;出来的是名为 request_user_input_options 的快照对比。

调用关系:测试框架运行它;它通过 render_snapshot 和 insta 把真实绘制结果固定下来,保护基础 UI 布局。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_options_notes_visible_snapshot3243–3263 ↗
fn request_user_input_options_notes_visible_snapshot()

作用:保存选择题打开备注框时的标准画面。它防止备注区显示被无意改坏。

数据流:进去的是一个选择题,测试先选中选项并按 Tab 打开备注;再渲染固定区域;出来的是备注可见状态的快照。

调用关系:测试框架调用它;它验证 handle_key_event 改变界面状态后,render_snapshot 产出的 UI 和预期一致。

调用图:调用 1 个内部函数(new);外部调用 6 个(from, new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_tight_height_snapshot3266–3280 ↗
fn request_user_input_tight_height_snapshot()

作用:检查高度很紧张时,弹窗仍然能合理显示。小终端窗口不能把界面挤坏。

数据流:进去的是一个选择题和 120x10 的小区域;测试渲染;出来的是 tight height 快照。

调用关系:测试框架运行它;它让布局和绘制函数在小高度场景下接受快照保护。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::layout_allocates_all_wrapped_options_when_space_allows3283–3308 ↗
fn layout_allocates_all_wrapped_options_when_space_allows()

作用:确认空间足够时,换行后的所有选项内容都能拿到需要的高度。长选项不应该被莫名截断。

数据流:进去的是带长换行选项的问题和宽度 48;测试计算问题高度、选项所需高度和页脚高度,再布局;出来的是 options_area.height 等于选项需要的完整高度。

调用关系:测试框架调用它;它验证 wrapped_question_lines、options_required_height、footer_required_height 和 layout_sections 的高度计算能对齐。

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

tests::desired_height_keeps_spacers_and_preferred_options_visible3311–3337 ↗
fn desired_height_keeps_spacers_and_preferred_options_visible()

作用:确认弹窗自己计算的理想高度,会保留区块之间的空行,并显示选项的偏好高度。这样画面不挤,也不浪费关键信息。

数据流:进去的是带换行选项的问题和宽度 110;测试拿 desired_height 算出内容区并布局;出来的是选项区高度等于偏好高度,问题后和选项后各有 1 行间隔。

调用关系:测试框架运行它;它验证 desired_height 和 layout_sections 使用同一套布局假设。

调用图:调用 2 个内部函数(new, menu_surface_inset);外部调用 5 个(new, assert_eq!, request_event, test_sender, vec!)。

tests::request_user_input_wrapped_options_snapshot3377–3407 ↗
fn request_user_input_wrapped_options_snapshot()

作用:保存长选项自动换行时的标准画面。它保证多行选项的缩进、选中标记和布局稳定。

数据流:进去的是带换行选项的问题,测试选中第一项并计算合适高度;渲染后;出来的是 wrapped options 快照。

调用关系:测试框架运行它;它把选项高度计算和实际渲染连在一起,用快照保护复杂文本排版。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_long_option_text_snapshot3410–3427 ↗
fn request_user_input_long_option_text_snapshot()

作用:保存超长选项文字显示时的标准画面。它防止长文本撑坏边界或显示混乱。

数据流:进去的是带很长选项文字的问题和 120x18 区域;测试渲染;出来的是 long option text 快照。

调用关系:测试框架调用它;它专门覆盖 render_snapshot 面对极长选项文本时的显示结果。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::selected_long_wrapped_option_stays_visible3430–3450 ↗
fn selected_long_wrapped_option_stays_visible()

作用:确认长列表里选中的长换行选项仍然出现在可视区域。用户移动到某项时,不能选中了却看不见。

数据流:进去的是一个需要滚动的长选项问题,测试选中第三项;渲染后;出来的文本中必须包含“› 3. Use Detailed Hint C”。

调用关系:测试框架运行它;它验证选项列表的滚动或裁剪逻辑会围绕当前选中项调整视口。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, assert!, render_snapshot, request_event, test_sender, vec!)。

tests::request_user_input_scroll_options_snapshot3481–3530 ↗
fn request_user_input_scroll_options_snapshot()

作用:这个测试检查:当选项很多、显示区域高度不够时,提问面板能不能正确滚动到当前选中的选项。它防止用户选到下面的项目时,界面却没有把它显示出来。

数据流:测试先造出一个假的提问事件,里面有一个问题和五个选项;再创建提问面板,并把当前选中的选项设成第 4 个。然后给它一个 120 列、12 行的显示区域,把界面渲染成文本快照。最后用快照断言确认渲染结果和预期一致;它不会真的发送用户答案,只是检查画面。

调用关系:它通过 test_sender 准备一个假的消息发送通道,用 request_event 组装假请求,再用 RequestUserInputOverlay::new 创建面板。最后把面板交给 render_snapshot 生成画面,并由 insta::assert_snapshot! 做对比。它模拟的是生产代码里“用户正在看一个选项列表并向下滚动”的场景。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_freeform_snapshot3585–3599 ↗
fn request_user_input_freeform_snapshot()

作用:这个测试检查没有预设选项的问题,也就是让用户自由输入文字的问题,初始界面是否正确。它保证用户看到的是输入框式的提示,而不是选项列表。

数据流:测试先创建一个没有选项的问题,标题是 Goal;再创建提问面板,并给它 120 列、10 行的显示区域。随后把面板渲染成快照,和保存的标准结果比较。进去的是一个自由输入问题和窗口大小,出来的是被确认正确的界面快照。

调用关系:它使用 question_without_options 构造“没有选项”的问题,再通过 request_event 包成一次请求,由 RequestUserInputOverlay::new 创建实际面板。render_snapshot 和 insta::assert_snapshot! 接手最后的画面检查。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_freeform_remapped_submit_snapshot3602–3619 ↗
fn request_user_input_freeform_remapped_submit_snapshot()

作用:这个测试检查用户把“提交答案”的快捷键改掉以后,界面底部的提示文字会不会跟着变。它防止界面还写着旧快捷键,导致用户按错。

数据流:测试先拿到默认快捷键配置,然后把提交键改成 Ctrl+J。接着用这个自定义键盘配置创建自由输入问题的提问面板,渲染到 120 列、10 行的区域里。最后用快照确认界面显示的是新的提交快捷键。它改变的是测试里的键位配置,不会影响全局运行环境。

调用关系:它比普通自由输入测试多走了一步 RuntimeKeymap::defaults 和 RequestUserInputOverlay::new_with_keymap:前者拿默认键位,后者把改过的键位交给面板。之后仍然由 render_snapshot 生成画面,再由 insta::assert_snapshot! 验证。

调用图:调用 2 个内部函数(new_with_keymap, defaults);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_freeform_remapped_interrupt_snapshot3622–3639 ↗
fn request_user_input_freeform_remapped_interrupt_snapshot()

作用:这个测试检查“中断当前操作”的快捷键被改成别的键后,提问面板的提示是否同步更新。它保证屏幕上告诉用户的按键和程序实际接受的按键一致。

数据流:测试先创建默认快捷键配置,再把中断当前回合的快捷键改成 F12。然后用这个键位配置创建一个自由输入面板,并把它渲染成固定大小的终端画面。最后快照对比会确认界面里显示了新的中断键。输入是改过的键位和一个假问题,输出是被验证的界面快照。

调用关系:它通过 RuntimeKeymap::defaults 准备可修改的键位表,通过 RequestUserInputOverlay::new_with_keymap 把键位表接入提问面板。测试链路最后仍交给 render_snapshot 和 insta::assert_snapshot!,重点看快捷键提示是否正确。

调用图:调用 2 个内部函数(new_with_keymap, defaults);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_multi_question_first_snapshot3642–3662 ↗
fn request_user_input_multi_question_first_snapshot()

作用:这个测试检查一次请求里有多个问题时,面板一开始是否显示第一个问题。它防止多问题表单刚打开就跳错位置。

数据流:测试创建一个包含两个问题的请求:第一个有选项,第二个是自由输入。然后创建面板,不做任何切换,直接用 120 列、15 行的区域渲染。快照会记录并检查当前显示的是第一个问题,以及多问题导航相关提示是否正确。

调用关系:它把 question_with_options 和 question_without_options 生成的两个问题一起交给 request_event,再由 RequestUserInputOverlay::new 建出多问题面板。render_snapshot 负责把当前状态画出来,insta::assert_snapshot! 负责确认“初始停在第一题”这个行为没变。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_multi_question_last_snapshot3665–3686 ↗
fn request_user_input_multi_question_last_snapshot()

作用:这个测试检查多问题请求切到最后一个问题后,界面是否正确显示最后一题。它保证“下一题”导航不会只改内部状态,却没有正确更新画面。

数据流:测试先创建一个有两道题的面板,然后调用 move_question,把当前题从第一题移动到下一题。接着把界面渲染到 120 列、12 行的区域,并和标准快照比较。进去的是两题请求和一次“切下一题”的动作,出来的是最后一题的界面显示结果。

调用关系:它先走和多问题初始测试相同的建模流程:question_with_options、question_without_options、request_event、RequestUserInputOverlay::new。不同点是它随后让 overlay.move_question 执行题目切换,再把切换后的状态交给 render_snapshot 验证。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::request_user_input_unanswered_confirmation_snapshot3689–3712 ↗
fn request_user_input_unanswered_confirmation_snapshot()

作用:这个测试检查用户还有问题没回答时,系统弹出的确认提示长什么样。它防止用户不小心提交不完整答案时,界面没有清楚提醒。

数据流:测试创建一个两题请求,但不填写答案。然后调用 open_unanswered_confirmation 打开“还有未回答问题”的确认界面。最后在 80 列、12 行的小区域里渲染并做快照对比。输入是未完成的多问题状态,输出是确认弹窗样式的界面快照。

调用关系:它先用 request_event 和 RequestUserInputOverlay::new 建出正常提问面板,再调用 overlay.open_unanswered_confirmation 进入确认状态。随后 render_snapshot 看到的就不只是普通问题界面,而是带确认提示的界面;insta::assert_snapshot! 用来锁定这个显示效果。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_snapshot!, request_event, test_sender, vec!)。

tests::options_scroll_while_editing_notes3715–3736 ↗
fn options_scroll_while_editing_notes()

作用:这个测试检查一个细节:用户正在编辑备注文字时,按向下键仍然能移动选项列表,但不会把当前选择当成正式提交。它防止“只是浏览选项”被误判成“已经确认答案”。

数据流:测试创建一个带选项的问题,先选中当前选项但标记为未正式提交;然后把焦点切到备注区,填入 Notes,并把光标移到末尾。接着模拟按下向下键。最后读取当前答案,确认选中项变成第 2 个,同时 answer_committed 仍是 false,也就是只移动了选择,没有提交答案。

调用关系:它用 test_sender、request_event 和 RequestUserInputOverlay::new 搭起面板,再直接操作面板状态来模拟“正在写备注”。随后 handle_key_event 接收一个向下键事件,走真实键盘处理路径。最后 current_answer 把处理后的答案状态拿出来,assert_eq! 和 assert! 验证结果。

调用图:调用 1 个内部函数(new);外部调用 7 个(from, new, assert!, assert_eq!, request_event, test_sender, vec!)。

tui/src/bottom_pane/pending_thread_approvals.rs源码 ↗
domain_logicmain loop / render

这个文件定义了一个叫 PendingThreadApprovals 的小组件。你可以把它想成终端界面底部的一张待办便签:如果某些不活跃的线程里有请求等着用户批准,它就显示“Approval needed in 某线程”。它只保存线程名字,不直接决定哪些请求需要批准;外面的系统把名单塞进来,它负责把名单画好。为了不把界面撑得太乱,它最多展示前三个线程,更多的就用省略号表示。它还会在最后加一句“/agent to switch threads”,提醒用户可以用这个命令切换线程去处理。显示时它会根据可用宽度自动换行,并用缩进和颜色让警告更醒目。宽度太窄或没有待处理线程时,它什么也不画,避免制造无意义的空白。

函数细节11
PendingThreadApprovals::new17–21 ↗
fn new() -> Self

作用:创建一个空的待批准线程提示组件。刚创建时里面没有任何线程,所以界面上也不会显示提示。

数据流:进去不需要任何参数 → 它准备一个空的字符串列表,用来以后存线程名字 → 出来一个新的 PendingThreadApprovals 对象,内部线程列表为空。

调用关系:这是这个小组件的起点。真实界面或测试在需要一个新的提示区时会调用它,之后再通过 PendingThreadApprovals::set_threads 填入具体线程。

调用图:被 4 处调用(new, desired_height_empty, render_multiple_threads_snapshot, render_single_thread_snapshot);外部调用 1 个(new)。

PendingThreadApprovals::set_threads23–29 ↗
fn set_threads(&mut self, threads: Vec<String>) -> bool

作用:把当前需要提醒用户处理的线程名单放进组件里。它还会判断名单有没有变化,方便外层界面决定要不要重新刷新。

数据流:进去一个线程名列表 → 它拿新列表和自己保存的旧列表比较;如果完全一样,就不改动并返回 false;如果不一样,就替换成新列表并返回 true → 出来的是“内容是否变化”的布尔值,同时组件内部的线程名单可能被更新。

调用关系:外层的 set_pending_thread_approvals 会在发现待批准线程变化时调用它。它不负责找出哪些线程需要批准,只负责接收最终名单,并告诉外层这次更新有没有必要触发重画。

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

PendingThreadApprovals::is_empty31–33 ↗
fn is_empty(&self) -> bool

作用:告诉别人现在有没有需要显示的待批准线程。外层可以用它决定这块提示区要不要占地方。

数据流:进去不需要额外参数,只读取组件内部的线程列表 → 它检查列表是不是空的 → 出来 true 或 false,true 表示没有任何待处理线程。

调用关系:它会被 as_renderable_with_composer_right_reserve 这类外层布局代码使用。布局代码先问它“你有没有内容”,再决定底部区域怎么安排。

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

PendingThreadApprovals::threads36–38 ↗
fn threads(&self) -> &[String]

作用:在测试环境里把组件内部保存的线程名单拿出来看看。它主要是为了测试能确认数据有没有被正确保存。

数据流:进去不需要额外参数 → 它直接读取内部线程列表 → 出来一个只读的线程名切片,也就是不能随便改原数据的列表视图。

调用关系:这个函数只在测试编译时存在,会被 pending_thread_approvals 相关测试使用。正常运行的用户界面不会靠它工作。

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

PendingThreadApprovals::as_renderable40–70 ↗
fn as_renderable(&self, width: u16) -> Box<dyn Renderable>

作用:把“线程名单”变成真正能画到终端屏幕上的内容。它负责决定显示哪些行、怎么缩进、超长时怎么换行,以及颜色提示怎么加。

数据流:进去一个可用宽度 width,并读取内部线程列表 → 如果没有线程或宽度小于 4,就生成一个空内容;否则取最多前三个线程,为每个线程拼出“Approval needed in ...”,再按宽度自动换行;如果线程超过三个,就加一行省略号;最后加一行“/agent to switch threads”的操作提示 → 出来一个 Renderable,也就是可以被画出来的界面对象。

调用关系:PendingThreadApprovals::render 和 PendingThreadApprovals::desired_height 都会先调用它。它像一个排版工,把原始名单整理成可显示的段落;真正画到缓冲区和计算高度的动作,则交给返回的 Renderable 去做。

调用图:调用 2 个内部函数(new, adaptive_wrap_lines);被 2 处调用(desired_height, render);外部调用 7 个(new, from, new, new, format!, once, vec!)。

PendingThreadApprovals::render74–80 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把这个提示组件画到终端界面的指定区域里。它是组件真正出现在屏幕上的入口。

数据流:进去一个屏幕区域 area 和一个绘图缓冲区 buf;缓冲区可以理解成先写好的草稿纸,最后再显示到终端 → 如果区域是空的,它直接返回;否则按区域宽度生成可绘制内容,再把内容画进缓冲区 → 出来没有单独返回值,但 buf 里的对应位置会被写上提示文字和样式。

调用关系:它实现了 Renderable 接口,所以外层渲染系统可以像画其他组件一样画它。测试里的 tests::snapshot_rows 也会调用它,把画出来的结果转成文字快照来检查。

调用图:调用 1 个内部函数(as_renderable);被 1 处调用(snapshot_rows);外部调用 1 个(is_empty)。

PendingThreadApprovals::desired_height82–84 ↗
fn desired_height(&self, width: u16) -> u16

作用:计算这个提示组件在给定宽度下需要占多少行。终端界面在排版前需要知道它有多高,才不会和别的内容挤在一起。

数据流:进去一个宽度 width,并读取当前线程列表 → 它先按这个宽度生成可绘制内容,再询问这份内容需要几行 → 出来一个数字,表示建议占用的高度。

调用关系:布局和测试会用它来预估空间。tests::snapshot_rows 会先调用它确定缓冲区高度,再调用 PendingThreadApprovals::render 真正绘制。

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

tests::snapshot_rows93–106 ↗
fn snapshot_rows(widget: &PendingThreadApprovals, width: u16) -> String

作用:这是测试辅助函数,用来把组件画出来的结果转成普通字符串,方便和快照对比。快照测试就像给界面拍一张标准照片,以后改代码时看画面有没有变。

数据流:进去一个 PendingThreadApprovals 组件和一个宽度 → 它先问组件需要多高,再创建同样大小的空缓冲区,然后调用组件的 render 把内容画进去;最后逐格读取缓冲区里的字符,拼成多行字符串 → 出来一段表示界面外观的文本。

调用关系:它服务于下面几个测试函数。测试函数不用直接理解缓冲区怎么工作,只要通过它拿到一段可比较的文本,再交给快照断言检查。

调用图:调用 2 个内部函数(desired_height, render);外部调用 2 个(empty, new)。

tests::desired_height_empty109–112 ↗
fn desired_height_empty()

作用:测试空组件在界面上不应该占高度。也就是说,没有待处理线程时,这块提示区应该完全不打扰用户。

数据流:进去不需要外部输入 → 它创建一个新的空 PendingThreadApprovals,然后询问宽度为 40 时需要多高 → 期望出来的高度是 0,如果不是,测试就失败。

调用关系:它调用 PendingThreadApprovals::new 创建测试对象,再用断言检查 PendingThreadApprovals::desired_height 的结果是否符合预期。

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

tests::render_single_thread_snapshot115–126 ↗
fn render_single_thread_snapshot()

作用:测试只有一个待批准线程时,界面提示长什么样。它确保警告行和“/agent”切换提示都会正确出现。

数据流:进去不需要外部输入 → 它创建组件,放入一个线程名“Robie [explorer]”,再用固定宽度把组件画成文本;然后把空格替换成点,方便看清对齐 → 出来的文本会和保存好的快照比较,画面变了就会提醒开发者。

调用关系:它依赖 PendingThreadApprovals::new 创建组件,并通过 tests::snapshot_rows 间接走完整的高度计算和渲染流程。assert_snapshot! 负责保存和比对预期画面。

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

tests::render_multiple_threads_snapshot129–148 ↗
fn render_multiple_threads_snapshot()

作用:测试多个待批准线程时的显示规则,特别是“最多显示三个,多出来用省略号”的行为。这样可以防止以后改代码时把底部提示画得太长或漏掉操作提示。

数据流:进去不需要外部输入 → 它创建组件,放入四个线程名,再按固定宽度画成文本;因为超过三个线程,预期画面里会有前三个线程、一行省略号,以及最后的“/agent”提示 → 出来的文本和快照比较,用来确认排版没有意外变化。

调用关系:它和单线程快照测试一样,先用 PendingThreadApprovals::new 准备组件,再通过 tests::snapshot_rows 触发 PendingThreadApprovals::desired_height 和 PendingThreadApprovals::render,最后交给 assert_snapshot! 检查结果。

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

聊天组件批准流程

这些聊天组件模块将批准事件转换为具体的弹窗流程、权限选择、沙盒提示,以及可在转录中显示的请求处理。

tui/src/chatwidget/permission_popups.rs源码 ↗
orchestration用户在聊天界面打开权限菜单、确认危险权限或处理自动审核拒绝时活跃

这份文件像是聊天界面里的“权限开关面板”。模型要执行命令、改文件或联网时,权限设置很关键:设得太严会做不了事,设得太松又可能误删文件、泄露数据。这里的代码会根据当前配置、操作系统和功能开关,拼出一个可选列表,显示给用户。用户点某一项后,它不会直接到处改状态,而是发送应用事件,让主程序统一处理。文件里还特别处理了几个敏感场景:比如“完全访问”会先弹出红色警告;自动审核拒绝过的操作可以从列表里挑出来批准一次;Windows 上还会根据沙箱状态给出额外提示。整体来说,它是权限选择、危险确认、自动审核补救这几条用户流程的组织者。

函数细节9
ChatWidget::open_approvals_popup11–13 ↗
fn open_approvals_popup(&mut self)

作用:这是打开“审批设置”弹窗的入口。它现在只是把用户带到通用的权限弹窗里,等于给旧名字保留了一个门牌。

数据流:进去的是当前的 ChatWidget 状态 → 它不自己生成界面,只是调用打开权限弹窗的函数 → 出来是底部面板显示权限选择界面,具体内容由后续函数决定。

调用关系:它把活儿交给 ChatWidget::open_permissions_popup。这样不管用户是从“审批”入口还是“权限”入口进来,最后都走同一套弹窗流程,避免两边显示不一致。

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

ChatWidget::open_permissions_popup16–160 ↗
fn open_permissions_popup(&mut self)

作用:这个函数真正组装“更新模型权限”的选择弹窗。它会把当前权限、可用预设、平台差异和功能开关都考虑进去,然后给用户一组选项。

数据流:进去的是 ChatWidget 里的配置、当前工作目录、功能开关和当前权限状态 → 它读取内置权限预设,判断哪些该显示、哪些该禁用、哪一项是当前正在用的,还为每个选项准备点击后的动作 → 出来是一个显示在底部面板里的选择列表;如果配置要求显式选择权限档案,它会先转去打开权限档案弹窗。

调用关系:ChatWidget::open_approvals_popup 会调用它。它在组装每个选项时会调用 ChatWidget::permission_mode_actions 来决定点击后做什么,也会用 ChatWidget::preset_matches_current 判断某个预设是不是当前设置;在 Windows 上还会读取沙箱级别,决定是否显示额外提醒。

调用图:调用 3 个内部函数(from, permission_mode_actions, level_from_config);被 1 处调用(open_approvals_popup);外部调用 7 个(new, default, preset_matches_current, new, cfg!, format!, matches!)。

ChatWidget::open_auto_review_denials_popup162–220 ↗
fn open_auto_review_denials_popup(&mut self)

作用:这个函数打开“自动审核拒绝记录”的弹窗。用户可以看到最近哪些操作被自动审核挡下,并选择其中一个批准重试一次。

数据流:进去的是当前聊天里保存的自动审核拒绝记录和线程编号 → 如果没有记录,它显示一条说明;如果线程不存在,它显示错误;如果有记录,它把每条拒绝整理成一行,包含命令摘要和拒绝理由 → 出来是一个可搜索的列表,用户选中某条后会发送批准该拒绝记录的事件,并请求界面重绘。

调用关系:它是自动审核流程的补救入口。它本身不直接批准操作,而是在用户选中某条记录时发送 AppEvent::ApproveRecentAutoReviewDenial,后续会进入 ChatWidget::approve_recent_auto_review_denial 这样的处理流程。

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

ChatWidget::approve_recent_auto_review_denial222–239 ↗
fn approve_recent_auto_review_denial(&mut self, thread_id: ThreadId, id: String)

作用:这个函数把用户选中的一条“自动审核拒绝”登记为允许重试一次。它不是永久放行,而是给模型下一次尝试提供一份批准上下文。

数据流:进去的是线程编号和拒绝记录的 id → 它先从最近拒绝记录里取出对应事件;如果找不到,就提示这条记录已经不可用;如果找到了,就生成一个批准被拒操作的线程命令并发送出去 → 出来是应用收到一次提交线程操作的事件,同时界面显示“已记录批准”的提示。

调用关系:它承接自动审核拒绝弹窗里的用户选择。真正构造“批准被 Guardian 拒绝的动作”的命令时,它调用 AppCommand::approve_guardian_denied_action,之后把命令交给应用事件通道处理。

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

ChatWidget::approval_preset_actions241–275 ↗
fn approval_preset_actions(
        approval: AskForApproval,
        permission_profile: PermissionProfile,
        active_permission_profile: ActivePermissionProfile,
        label: String,

作用:这个函数把一个权限预设变成“用户点选后要执行的一串动作”。简单说,它负责把界面上的一个选项,包装成真正会更新权限状态的按钮动作。

数据流:进去的是审批策略、权限档案、当前激活档案、显示标签和由谁审核的设置 → 它创建一个闭包,也就是稍后点击时才运行的小动作包 → 出来是一组选择动作;执行时会发送更新会话上下文、更新审批策略、更新权限档案、更新审核者,并插入一条“权限已更新”的历史提示。

调用关系:它被 ChatWidget::permission_mode_actions 和 ChatWidget::open_full_access_confirmation 用来生成“应用这个权限预设”的动作。它不显示界面,只负责把选择结果变成应用事件。

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

ChatWidget::permission_profile_selection_actions277–283 ↗
fn permission_profile_selection_actions(
        selection: PermissionProfileSelection,
    ) -> Vec<SelectionAction>

作用:这个函数把“选择某个权限档案”包装成一个可点击动作。它适合那些不是直接套用内置预设,而是让用户选具体档案的场景。

数据流:进去的是一个权限档案选择结果 → 它把这个选择包进一个稍后执行的动作里 → 出来是一组选择动作;执行时会发送 AppEvent::SelectPermissionProfile,让主程序继续处理这个档案选择。

调用关系:它和 ChatWidget::approval_preset_actions 是两种分支:一个用于直接应用预设,一个用于进入或应用权限档案选择。ChatWidget::permission_mode_actions 和 ChatWidget::open_full_access_confirmation 会根据情况选择用哪一个。

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

ChatWidget::permission_mode_actions285–366 ↗
fn permission_mode_actions(
        &self,
        preset: &ApprovalPreset,
        label: String,
        approvals_reviewer: ApprovalsReviewer,
        profile_selection: Option<PermissionProfileSel

作用:这个函数决定用户点某个权限模式后,应该直接应用,还是先弹警告、先启用 Windows 沙箱、或先处理其他安全提示。它是权限菜单里最重要的“分岔路口”。

数据流:进去的是某个权限预设、显示标签、审核者、可选的档案选择,以及是否要回到权限菜单的标记 → 它先准备好最终应用权限的动作;然后检查是否属于高风险场景,比如完全访问需要确认,Windows 自动模式可能需要启用沙箱,或需要提示世界可写目录风险 → 出来要么是直接应用权限的一组动作,要么是一组先打开确认/提示弹窗的动作。

调用关系:ChatWidget::open_permissions_popup 为每个权限选项调用它。它需要真正应用预设时,会转用 ChatWidget::approval_preset_actions;需要选择档案时,会转用 ChatWidget::permission_profile_selection_actions;在 Windows 场景下还会查看沙箱配置和安装状态,把用户带到相应的沙箱提示流程。

调用图:调用 1 个内部函数(level_from_config);被 1 处调用(open_permissions_popup);外部调用 3 个(sandbox_setup_is_complete, clone, vec!)。

ChatWidget::preset_matches_current368–405 ↗
fn preset_matches_current(
        current_approval: AskForApproval,
        current_permission_profile: &PermissionProfile,
        cwd: &std::path::Path,
        preset: &ApprovalPreset,
    ) -> bo

作用:这个函数判断某个权限预设是不是当前正在使用的设置。它让弹窗能正确标出“当前选中”的那一项。

数据流:进去的是当前审批策略、当前权限档案、当前工作目录和一个候选预设 → 它先比较审批策略是否一致,再按预设类型细看文件写入权限、网络权限和当前目录是否可写 → 出来是 true 或 false,表示这个预设是否和当前状态相匹配。

调用关系:ChatWidget::open_permissions_popup 在生成列表时会用它。这个判断不是简单比名字,因为像“自动”和“只读”这类模式要看文件系统沙箱策略、网络沙箱策略以及当前目录,所以它会调用权限档案里的文件系统和网络策略检查方法。

调用图:调用 3 个内部函数(from, file_system_sandbox_policy, network_sandbox_policy);外部调用 1 个(matches!)。

ChatWidget::open_full_access_confirmation407–500 ↗
fn open_full_access_confirmation(
        &mut self,
        preset: ApprovalPreset,
        return_to_permissions: bool,
        profile_selection: Option<PermissionProfileSelection>,
    )

作用:这个函数打开“是否启用完全访问”的危险确认弹窗。它用明确的警告告诉用户:完全访问会让模型不用再询问就能改很多文件、运行联网命令。

数据流:进去的是用户想启用的完全访问预设、取消后回到哪里的标记,以及可选的权限档案选择 → 它生成带红色警告文字的弹窗头部,准备三个选项:继续一次、继续并以后不再提醒、取消 → 出来是底部面板显示确认弹窗;如果用户继续,会发送更新权限和记录已确认的事件;如果选择记住,还会持久保存这个确认;如果取消,会回到权限或审批弹窗。

调用关系:ChatWidget::permission_mode_actions 发现用户要启用 full-access 且还没隐藏警告时,会让应用打开这个弹窗。它内部复用 ChatWidget::approval_preset_actions 或 ChatWidget::permission_profile_selection_actions 来完成真正的权限应用,只是在前面加了一道安全确认。

调用图:调用 2 个内部函数(from, with);外部调用 6 个(new, default, from, new, new, vec!)。

tui/src/chatwidget/permissions_menu.rs源码 ↗
domain_logic用户打开权限菜单时

这个文件是 ChatWidget 的一小块界面逻辑,专门负责拼出“更新模型权限”的弹窗菜单。它先从当前配置里找出正在使用的权限档案,再取出内置的几种权限预设,比如普通工作区模式、只读模式、完全访问模式。找不到关键预设时,它不会继续瞎画菜单,而是直接显示内部错误。接着它把每个预设变成弹窗里的一行选项,并标出哪一项是当前正在用的;如果开启了 GuardianApproval 功能,还会多放一个“自动帮我审查”的选项。最后它把用户自己配置的权限档案也追加进去,并交给底部面板显示。可以把它想成餐厅菜单:先确认招牌菜都在,再把默认菜、自定义菜排好,标出当前点的是哪一道。

函数细节3
ChatWidget::open_permission_profiles_popup4–87 ↗
fn open_permission_profiles_popup(&mut self)

作用:打开“更新模型权限”的弹窗菜单。用户想切换模型权限时会用到它,它负责把内置权限和自定义权限都整理成可点击的选项。

数据流:进去的是当前 ChatWidget,里面带着配置、权限状态和底部面板。它先读取当前激活的权限档案,再拿到内置权限预设,检查只读、自动、完全访问这几个必需项是否存在;如果缺了,就往聊天里加一条错误消息并停止。没问题时,它把这些预设和用户自定义档案转换成菜单项,最后让 bottom_pane 显示一个带标题、提示语和选项列表的弹窗。

调用关系:这是整个权限菜单的入口。它会调用 ChatWidget::builtin_permission_mode_selection_item 来生成内置权限模式的菜单行;菜单项准备好后,它把显示工作交给底部面板。它也会根据配置决定是否加入自动审查相关的选项。

调用图:调用 2 个内部函数(from, builtin_permission_mode_selection_item);外部调用 3 个(new, default, vec!)。

ChatWidget::builtin_permission_mode_selection_item89–147 ↗
fn builtin_permission_mode_selection_item(
        &self,
        preset: &ApprovalPreset,
        id: &str,
        description: String,
        approval_policy: AskForApproval,
        approvals_rev

作用:把一个内置权限预设变成弹窗里的一行选项。它不只是写个名字,还会判断这一行是不是当前正在使用、能不能被选择,以及点了之后该执行什么动作。

数据流:进去的是一个内置预设、要显示的编号和说明、审批策略以及由谁来审查。审批策略就是“模型做某些事前要不要先问人”;审查者可以是用户,也可以是自动审查。函数会读取当前配置,比较当前权限档案、当前审批策略和当前审查者,决定这一行是否标为当前项。然后它生成 SelectionItem,也就是弹窗用的一行数据,里面包含名称、说明、点击动作、是否禁用以及禁用原因。

调用关系:它由 ChatWidget::open_permission_profiles_popup 调用,是内置权限选项的加工器。它会把真正点击后要做的事情交给 permission_mode_actions,并用权限配置里的检查结果决定这行能不能点。

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

ChatWidget::permission_profile_selection_item149–170 ↗
fn permission_profile_selection_item(
        label: &str,
        id: &str,
        description: &str,
        active_profile_id: Option<&str>,
    ) -> SelectionItem

作用:把用户自定义的权限档案变成弹窗里的一行选项。这样用户不只能选系统内置模式,也能选择自己在配置里写好的权限方案。

数据流:进去的是显示名称、档案编号、说明文字,以及当前激活的档案编号。它把这些信息包成 PermissionProfileSelection,也就是“用户选中了哪个权限档案”的记录,再生成 SelectionItem。出来的是一行可显示、可点击的菜单项;如果它的编号等于当前激活编号,就会被标成当前项。

调用关系:它是自定义权限档案的菜单行生成器。生成点击动作时,它把后续工作交给 permission_profile_selection_actions;这个动作会在用户选中该档案后真正应用对应的权限选择。

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

tui/src/chatwidget/windows_sandbox_prompts.rs源码 ↗
orchestrationstartup / permission change / Windows sandbox setup

在 Windows 上,Codex 会用“沙箱”(一个隔离区,用来限制程序能改哪些文件、能不能联网)来保护用户电脑。这个文件就是 ChatWidget 里和这些安全提示有关的一组按钮弹窗。它会判断当前配置是否允许某种沙箱模式,检查管理员权限版沙箱是不是还没设置好,也会发现一种特殊风险:某些文件夹被“Everyone”可写,也就是电脑上几乎任何人或程序都能写,沙箱就不一定能挡住写入。发现问题后,它会在底部面板弹出选择:继续、记住不再提醒、安装默认沙箱、改用非管理员沙箱、重试或退出。用户点按钮后,这里不会直接把所有事做完,而是发送 AppEvent(应用内部事件,像递交一张工单),让后面的流程去真正安装沙箱、切权限或退出。非 Windows 系统上,这些函数大多什么也不做,避免影响其他平台。

函数细节9
ChatWidget::windows_sandbox_mode_allowed7–14 ↗
fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool

作用:这个函数用来问一句:当前配置规则允许把 Windows 沙箱切到某个模式吗?它很重要,因为有些组织配置可能禁止用户选择风险更高的模式。

数据流:进去的是一个想设置的沙箱模式。函数读取 ChatWidget 里的配置层规则,看这个模式能不能被设置;如果规则检查成功,就返回 true,否则返回 false。它不改界面,也不改配置,只做判断。

调用关系:打开“启用 Windows 沙箱”提示和“沙箱安装失败后的兜底提示”时都会先调用它。这样弹窗才能决定要不要给用户显示“使用非管理员沙箱”这个选项。

调用图:被 2 处调用(open_windows_sandbox_enable_prompt, open_windows_sandbox_fallback_prompt)。

ChatWidget::elevated_windows_sandbox_setup_required17–27 ↗
fn elevated_windows_sandbox_setup_required(&self) -> bool

作用:这个函数判断:管理员权限版 Windows 沙箱是不是现在必须设置,而且还没设置好。简单说,它是在确认“是不是非得让用户先完成沙箱安装”。

数据流:它读取当前配置,先通过 level_from_config 判断沙箱级别是不是 Elevated,也就是需要管理员权限的默认沙箱;再看这个要求是不是来自配置;最后检查 codex_home 目录里沙箱设置是否已经完成。三个条件都满足时,返回 true,表示还需要设置。

调用关系:maybe_prompt_windows_sandbox_enable 用它决定启动时要不要弹安装提示;启用提示和失败兜底提示也用它决定这个选择是不是强制的。如果是强制的,用户取消弹窗后还会重新打开提示。

调用图:调用 1 个内部函数(level_from_config);被 3 处调用(maybe_prompt_windows_sandbox_enable, open_windows_sandbox_enable_prompt, open_windows_sandbox_fallback_prompt);外部调用 1 个(sandbox_setup_is_complete)。

ChatWidget::world_writable_warning_details65–67 ↗
fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)>

作用:这个函数检查 Windows 下有没有“所有人都能写”的危险文件夹,并决定是否需要弹警告。没有这个检查时,用户可能以为沙箱能保护某些目录,但实际上这些目录权限太宽,保护效果无法保证。

数据流:它先看配置里有没有要求隐藏这个警告;如果隐藏,就直接返回 None。否则它读取当前工作目录、工作区根目录、系统环境变量和当前权限配置,把权限配置转换成 Windows 沙箱能理解的权限表,然后调用扫描函数检查风险路径。如果扫描和保护规则都没问题,返回 None;如果扫描失败或发现无法验证保护,就返回一些警告信息,用来给弹窗显示。非 Windows 平台固定返回 None。

调用关系:它是弹出“world-writable”警告前的探测步骤。它把配置、环境变量和权限档案交给 Windows 沙箱库去扫描,调用者拿到结果后再决定是否调用 open_world_writable_warning_confirmation 展示确认窗口。

调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots);外部调用 3 个(new, apply_world_writable_scan_and_denies_for_permissions, vars)。

ChatWidget::open_world_writable_warning_confirmation209–217 ↗
fn open_world_writable_warning_confirmation(
        &mut self,
        _preset: Option<ApprovalPreset>,
        _profile_selection: Option<PermissionProfileSelection>,
        _sample_paths: Vec<Stri

作用:这个函数打开一个确认弹窗,告诉用户某些文件夹权限太宽,Windows 沙箱可能保护不了写入。它让用户选择只是继续,还是继续并记住以后别再提醒。

数据流:进去的是可能要应用的权限预设、权限档案选择、示例路径、额外路径数量,以及扫描是否失败。函数先把当前模式翻译成人能看懂的名字,比如 Full Access、Agent 或 Read-Only;再拼出说明文字和最多几条例子;然后准备两个按钮。用户点按钮后,会发送内部事件:可能跳过下一次重复扫描、切换权限档案、应用审批预设,或者把“已确认,不再提醒”保存起来。最后它把这些内容交给 bottom_pane 显示。

调用关系:它通常接在 world_writable_warning_details 之后使用。它本身不真正改权限,而是把用户的选择包装成 SelectionAction;这些 action 会调用 permission_profile_selection_actions 或 approval_preset_actions 生成后续动作,再通过 AppEventSender 发给应用主流程。

调用图:调用 2 个内部函数(from, with);外部调用 9 个(new, default, from, new, approval_preset_actions, permission_profile_selection_actions, new, format!, vec!)。

ChatWidget::open_windows_sandbox_enable_prompt328–333 ↗
fn open_windows_sandbox_enable_prompt(
        &mut self,
        _preset: ApprovalPreset,
        _profile_selection: Option<PermissionProfileSelection>,
    )

作用:这个函数打开“请设置 Windows 沙箱”的主提示。它用于首次发现沙箱没准备好时,让用户在默认管理员沙箱、非管理员沙箱和退出之间做选择。

数据流:进去的是一个审批预设和可选的权限档案选择。函数先记录一次遥测计数,也就是给开发者统计“这个提示显示过”;再检查是否允许非管理员沙箱,以及管理员沙箱设置是否必须完成。然后它生成说明文字和按钮。用户选择管理员沙箱时,发送 BeginWindowsSandboxElevatedSetup;选择非管理员沙箱时,发送 BeginWindowsSandboxLegacySetup;选择退出时,发送 Exit。如果这个设置是强制的,取消弹窗会重新打开同一个提示。

调用关系:maybe_prompt_windows_sandbox_enable 会在需要时调用它。它内部调用 windows_sandbox_mode_allowed 和 elevated_windows_sandbox_setup_required 来决定弹窗内容,并把真正的安装工作交给后续 AppEvent 处理流程。

调用图:调用 3 个内部函数(elevated_windows_sandbox_setup_required, windows_sandbox_mode_allowed, new);被 1 处调用(maybe_prompt_windows_sandbox_enable);外部调用 5 个(new, default, new, clone, vec!)。

ChatWidget::open_windows_sandbox_fallback_prompt452–457 ↗
fn open_windows_sandbox_fallback_prompt(
        &mut self,
        _preset: ApprovalPreset,
        _profile_selection: Option<PermissionProfileSelection>,
    )

作用:这个函数在管理员权限版沙箱设置失败后显示兜底弹窗。它告诉用户失败了,并提供重试、改用非管理员沙箱或退出。

数据流:进去的是原本要应用的审批预设和可选权限档案选择。函数先判断非管理员沙箱是否被允许,以及管理员沙箱是否仍然必须;然后生成一段失败说明。它准备按钮:重试会发送 BeginWindowsSandboxElevatedSetup,改用非管理员沙箱会发送 BeginWindowsSandboxLegacySetup,退出会发送 Exit。每个选择也会记录对应的遥测计数。

调用关系:它通常在管理员沙箱安装流程失败后由事件流程打开。它和 open_windows_sandbox_enable_prompt 用同一套判断函数:windows_sandbox_mode_allowed 决定有没有替代选项,elevated_windows_sandbox_setup_required 决定用户能不能取消。

调用图:调用 3 个内部函数(elevated_windows_sandbox_setup_required, windows_sandbox_mode_allowed, new);外部调用 7 个(new, default, new, new, line!, clone, vec!)。

ChatWidget::maybe_prompt_windows_sandbox_enable475–475 ↗
fn maybe_prompt_windows_sandbox_enable(&mut self, _show_now: bool)

作用:这个函数负责在合适的时候自动决定要不要弹出 Windows 沙箱启用提示。它像一个门卫:只有当前确实需要设置沙箱,并且允许立刻显示时,才把弹窗打开。

数据流:进去的是 show_now,表示现在是否可以直接弹提示。函数读取配置算出当前 Windows 沙箱级别,再判断沙箱是关闭状态,或者管理员沙箱还没设置好。如果满足条件,它从内置审批预设里找 id 为 auto 的预设,然后调用 open_windows_sandbox_enable_prompt 打开提示。非 Windows 平台什么也不做。

调用关系:它是自动提示流程的入口之一。它调用 level_from_config 和 elevated_windows_sandbox_setup_required 做判断,最后把展示工作交给 open_windows_sandbox_enable_prompt。

调用图:调用 3 个内部函数(elevated_windows_sandbox_setup_required, open_windows_sandbox_enable_prompt, level_from_config)。

ChatWidget::show_windows_sandbox_setup_status499–499 ↗
fn show_windows_sandbox_setup_status(&mut self)

作用:这个函数在 Windows 沙箱正在安装时更新界面状态。它会临时禁用输入,避免用户在沙箱模式还没确定时发消息。

数据流:它不需要额外输入,直接修改 ChatWidget 的底部面板:关闭输入框,显示状态指示器,隐藏中断提示,设置“正在设置沙箱”的状态文字,然后请求界面重绘。结果是用户看到安装中的提示,也暂时不能输入新内容。

调用关系:它会在开始管理员沙箱设置流程时被调用,用来给用户一个明确的等待状态。真正的安装不是它做的,它只负责让界面进入“请稍等,别操作”的安全状态。

ChatWidget::clear_windows_sandbox_setup_status510–510 ↗
fn clear_windows_sandbox_setup_status(&mut self)

作用:这个函数在 Windows 沙箱设置流程结束后恢复界面。它把之前为了安全而锁住的输入框重新打开。

数据流:它不接收额外输入,直接把底部面板的输入功能重新启用,清掉占位提示,隐藏状态指示器,并请求重新绘制界面。结果是用户可以继续正常聊天。

调用关系:它和 show_windows_sandbox_setup_status 配对使用:一个在安装开始时让界面进入等待状态,一个在安装完成、失败或结束后把界面恢复。

tui/src/chatwidget/tool_requests.rs源码 ↗
orchestrationrequest handling

在这个聊天界面里,AI 有时不能直接行动:比如要执行命令、修改文件、访问网络、向外部工具提交信息,或者需要用户补充答案。这个文件就是这些“先问用户”的入口。它先记录当前对话有了可见活动,然后决定请求是马上显示,还是先放进队列等当前状态合适时再处理。真正显示时,它会把正在输出的回答先收尾,避免提示框插在半句话中间;再把请求塞到底部面板,让用户选择批准、拒绝、填写内容等。它还会发通知、让小宠物状态变成“等待”、要求界面重画。比较特别的是 guardian review,也就是自动安全审查:它会在底部状态栏显示“正在审查什么”,审查结束后再把批准、拒绝或超时结果写进历史记录,像给用户留一张收据。

函数细节13
ChatWidget::on_exec_approval_request9–16 ↗
fn on_exec_approval_request(&mut self, _id: String, ev: ExecApprovalRequestEvent)

作用:收到“能不能执行这条命令”的请求时调用。它不会直接默认同意,而是把请求交给界面流程,确保用户有机会看清楚再决定。

数据流:进去的是一个执行命令审批事件和请求编号;函数先记录这轮对话有新活动,然后复制一份事件。之后它把原事件放进可能的等待队列,或者在可以立刻处理时把复制出来的事件交给即时处理函数。出来没有返回值,但界面之后会出现命令审批提示。

调用关系:这是后台事件进入聊天窗口的第一站。它通过事件的 clone 复制一份数据,以便同一个请求既能排队也能马上处理;真正把提示放到底部面板的活儿由 ChatWidget::handle_exec_approval_now 做。

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

ChatWidget::on_apply_patch_approval_request18–29 ↗
fn on_apply_patch_approval_request(
        &mut self,
        _id: String,
        ev: ApplyPatchApprovalRequestEvent,
    )

作用:收到“能不能把这些补丁改到文件里”的请求时调用。它保证改文件这种敏感操作会先显示给用户确认。

数据流:进去的是补丁审批事件和请求编号;函数记录可见活动,复制事件,然后根据当前界面状态选择:要么先排队,要么马上处理。它本身不改文件,只安排界面显示审批请求。

调用关系:这是文件修改审批事件的入口。它依靠 clone 保留一份事件给不同处理路径使用;如果能立即展示,就把工作交给 ChatWidget::handle_apply_patch_approval_now。

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

ChatWidget::on_guardian_assessment38–254 ↗
fn on_guardian_assessment(&mut self, ev: GuardianAssessmentEvent)

作用:处理 guardian 自动安全审查的状态变化。guardian 可以理解成一个安全检查员,它会判断某个动作是否能做、被拒绝,或者等太久超时。

数据流:进去的是一条审查事件,里面有动作类型、审查编号和状态。函数先把动作翻译成用户能看懂的摘要,比如“访问某个网站”或“修改几个文件”。如果审查还在进行,它更新底部状态栏;如果审查结束,它先清掉对应的等待状态,再把批准、拒绝或超时结果做成历史记录卡片。出来没有返回值,但会改变状态栏、历史记录、最近拒绝列表,并触发界面重画。

调用关系:它是 guardian 审查生命周期的总调度点。进行中时,它主要更新状态提示;结束时,它会根据动作类型调用不同的历史卡片构造函数,比如 new_approval_decision_cell、new_guardian_denied_patch_request、new_guardian_timed_out_action_request 等,把机器判断变成用户能读懂的记录。

调用图:外部调用 12 个(from, format!, new_approval_decision_cell, new_guardian_approved_action_request, new_guardian_denied_action_request, new_guardian_denied_patch_request, new_guardian_timed_out_action_request, new_guardian_timed_out_patch_request, clone, to_string (+2 more))。

ChatWidget::on_elicitation_request256–268 ↗
fn on_elicitation_request(
        &mut self,
        request_id: AppServerRequestId,
        params: McpServerElicitationRequestParams,
    )

作用:收到外部 MCP 服务器向用户“追问信息”的请求时调用。MCP 可以理解成让 AI 连接外部工具的一套协议;elicitation 就是外部工具需要用户补充选择或填写内容。

数据流:进去的是请求编号和请求参数;函数记录可见活动,分别复制请求编号和参数。之后它要么把请求先排队,要么立刻交给即时处理函数。它本身不展示表单,只负责把请求送到合适的处理路径。

调用关系:这是 MCP 追问请求进入聊天界面的入口。它使用 clone 保留数据副本;真正决定打开链接视图、表单,还是普通审批框的逻辑在 ChatWidget::handle_elicitation_request_now。

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

ChatWidget::on_request_user_input270–277 ↗
fn on_request_user_input(&mut self, ev: ToolRequestUserInputParams)

作用:收到工具要求用户回答问题的请求时调用。它把“工具需要你回答几个问题”这件事接入聊天界面,而不是让请求无声地卡住。

数据流:进去的是用户输入请求参数,里面通常有一个或多个问题;函数记录活动,复制事件,然后选择排队或马上处理。出来没有直接结果,但后续底部面板会出现问题输入界面。

调用关系:这是用户输入请求的入口。它把请求交给队列机制;如果当前可以处理,就由 ChatWidget::handle_request_user_input_now 负责生成通知和底部输入区域。

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

ChatWidget::on_request_permissions279–286 ↗
fn on_request_permissions(&mut self, ev: RequestPermissionsEvent)

作用:收到“工具想申请更多权限”的请求时调用。它确保权限提升这类敏感事必须经过用户确认。

数据流:进去的是权限请求事件;函数记录当前对话活动,复制事件,然后把它排队或立即处理。函数不直接授予权限,只安排界面显示审批。

调用关系:这是权限申请事件的入口。它和其他审批入口一样使用 clone 复制事件;具体把权限请求变成底部审批卡片的工作交给 ChatWidget::handle_request_permissions_now。

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

ChatWidget::handle_exec_approval_now288–312 ↗
fn handle_exec_approval_now(&mut self, ev: ExecApprovalRequestEvent)

作用:立刻显示“是否允许执行命令”的审批提示。它把后台的命令请求整理成用户可以点击批准或拒绝的界面项目。

数据流:进去的是执行命令审批事件,包含命令、原因、可选决定、网络审批上下文等。函数先把正在输出的回答收尾,再把命令拼成一行文字用于通知;然后整理成 ApprovalRequest::Exec,放到底部面板,并把界面状态改成等待用户。出来没有返回值,但会发通知、更新底部面板、触发重画。

调用关系:通常由 ChatWidget::on_exec_approval_request 在可以立即处理时调用。它会用 effective_available_decisions 和 effective_approval_id 取出实际可用的按钮和审批编号,用 try_join 尽量把命令拼成适合人看的字符串,然后把请求交给 bottom_pane 显示。

调用图:调用 2 个内部函数(effective_approval_id, effective_available_decisions);外部调用 1 个(try_join)。

ChatWidget::handle_apply_patch_approval_now314–336 ↗
fn handle_apply_patch_approval_now(&mut self, ev: ApplyPatchApprovalRequestEvent)

作用:立刻显示“是否允许修改这些文件”的审批提示。它让用户在改动真正发生前看到要改哪里、为什么改。

数据流:进去的是补丁审批事件,包含调用编号、原因和文件改动内容。函数先收尾当前回答,再把这些信息包装成 ApprovalRequest::ApplyPatch,连同当前工作目录一起放到底部面板。随后它设置等待提示、重画界面,并发出“需要编辑审批”的通知,通知里包含目录和涉及文件。

调用关系:通常由 ChatWidget::on_apply_patch_approval_request 触发。它不自己应用补丁,只把补丁请求交给 bottom_pane,让用户决定下一步。

ChatWidget::handle_elicitation_request_now338–395 ↗
fn handle_elicitation_request_now(
        &mut self,
        request_id: AppServerRequestId,
        params: McpServerElicitationRequestParams,
    )

作用:立刻处理 MCP 服务器发来的追问请求。它会判断这个请求该打开链接、显示表单,还是降级成普通的审批提示。

数据流:进去的是请求编号和 MCP 追问参数,里面有服务器名、线程编号和具体请求内容。函数先收尾当前回答并发通知;然后把线程字符串转换成线程 ID。接着它优先尝试把请求变成应用链接视图;不行就尝试变成表单请求;再不行,如果是普通表单消息,就放到底部审批面板;如果是无法展示的 URL 请求,就直接回复拒绝。最后设置等待状态并重画界面。

调用关系:通常由 ChatWidget::on_elicitation_request 调用。它会借助 from_string 解析线程,借助 from_url_app_server_request 判断能不能打开应用链接,借助 from_app_server_request 判断能不能生成表单;如果都不合适,就自己把请求转成审批或通过 app_event_tx 回绝。

调用图:调用 3 个内部函数(from_string, from_url_app_server_request, from_app_server_request);外部调用 2 个(clone, clone)。

ChatWidget::push_approval_request397–405 ↗
fn push_approval_request(&mut self, request: ApprovalRequest)

作用:把一个已经整理好的审批请求直接放到底部面板。它是一个小入口,给其他代码复用“显示审批并进入等待状态”这套动作。

数据流:进去的是一个 ApprovalRequest,可能代表命令、补丁、权限或其他审批。函数把它交给底部面板显示,同时根据配置里的功能开关决定界面能力;然后把小宠物通知设为等待,并请求重画。出来没有返回值,但界面多了一个等待用户处理的审批项。

调用关系:它不像 on_* 函数那样接收原始后台事件,而是接收已经包装好的审批请求。其他地方如果已经准备好 ApprovalRequest,就可以跳过具体转换逻辑,直接用它推到界面上。

ChatWidget::push_mcp_server_elicitation_request407–418 ↗
fn push_mcp_server_elicitation_request(
        &mut self,
        request: McpServerElicitationFormRequest,
    )

作用:把一个已经整理好的 MCP 表单追问放到底部面板。它用于显示外部工具需要用户填写或选择的内容。

数据流:进去的是 McpServerElicitationFormRequest,也就是已经能直接展示的表单请求。函数把它放入底部面板,设置等待状态,并请求界面重画。出来没有返回值,但用户会看到一个待填写的 MCP 请求。

调用关系:这是显示 MCP 表单请求的简化入口。ChatWidget::handle_elicitation_request_now 在能把原始请求转换成表单时,会走类似的底部面板路径;这个函数则方便其他代码直接推送已经成形的表单。

ChatWidget::handle_request_user_input_now420–436 ↗
fn handle_request_user_input_now(&mut self, ev: ToolRequestUserInputParams)

作用:立刻显示工具提出的问题,让用户输入答案。它还会生成一个简短通知标题,告诉用户是一个问题还是多个问题。

数据流:进去的是用户输入请求,里面有问题列表。函数先收尾当前回答,数出问题数量,并尝试从问题内容生成摘要;然后根据数量和摘要做一个通知标题。接着它发出提示通知,把请求放到底部面板,设置等待状态,并重画界面。

调用关系:通常由 ChatWidget::on_request_user_input 在可以立即处理时调用。它会调用 user_input_request_summary 生成更友好的通知文字;如果问题很多,就用 format! 拼出“几个问题”的标题,然后交给 bottom_pane 展示输入请求。

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

ChatWidget::handle_request_permissions_now438–455 ↗
fn handle_request_permissions_now(&mut self, ev: RequestPermissionsEvent)

作用:立刻显示“是否允许申请这些权限”的审批提示。它把权限申请包装成用户能审核的卡片。

数据流:进去的是权限请求事件,包含调用编号、环境编号、申请原因和权限列表。函数先收尾当前回答,再把这些信息包装成 ApprovalRequest::Permissions,放到底部面板。随后它设置等待提示并请求重画。出来没有返回值,但界面会出现权限审批项。

调用关系:通常由 ChatWidget::on_request_permissions 调用。它不直接改变权限,而是把决定权交给用户;底部面板负责展示,后续用户操作再决定是否批准。

钩子审查 RPC

此辅助模块通过获取钩子元数据并持久化信任决策,支持钩子检查批准路径。

tui/src/hooks_rpc.rs源码 ↗
io_transportstartup and user actions

这里的 hook 可以理解成项目里会自动触发的小脚本或规则。因为它可能影响电脑上的操作,所以界面不能随便默认信任它,必须知道它是不是新来的、有没有被改过。这个文件做的事很具体:一是向后台询问当前目录有哪些 hooks;二是从后台返回的一大包结果里挑出当前目录那一份;三是判断某个 hook 是否需要用户复查;四是把用户确认信任的 hook 写进配置里。写配置时,它不是直接改文件,而是通过 app server 的 RPC(远程过程调用,意思是像调用本地函数一样请求另一个进程干活)发送“批量写配置”请求。这样界面只负责发命令,真正读写配置和校验版本的活交给后台,减少出错机会。

函数细节5
fetch_hooks_list24–36 ↗
async fn fetch_hooks_list(
    request_handle: AppServerRequestHandle,
    cwd: PathBuf,
) -> Result<HooksListResponse>

作用:向后台询问某个工作目录里有哪些 hooks。界面启动或需要刷新 hooks 状态时会用它。

数据流:进去的是一个后台请求通道和当前目录路径;它先生成一个唯一请求编号,再把目录放进 HooksList 请求里发给 app server;出来的是后台返回的 hooks 列表结果,如果请求失败,就带上“在 TUI 里 hooks/list 失败”的说明返回错误。

调用关系:它是界面拿 hooks 信息的入口之一。启动时的 hooks 复查流程会调用它,拿到结果后通常会交给 hooks_list_entry_for_cwd 挑出当前目录对应的数据。它自己把真正通信的活交给 request_typed。

调用图:调用 1 个内部函数(request_typed);被 2 处调用(fetch_hooks_list, load_startup_hooks_review_entry);外部调用 3 个(String, format!, vec!)。

hooks_list_entry_for_cwd38–49 ↗
fn hooks_list_entry_for_cwd(response: HooksListResponse, cwd: &Path) -> HooksListEntry

作用:从后台返回的 hooks 总列表里,找出某一个目录对应的那一条。这样界面不用自己在一堆目录结果里翻找。

数据流:进去的是后台返回的 HooksListResponse 和一个目录路径;它遍历 response.data,找到 cwd 跟目标目录相同的条目;如果没找到,就造一个空条目,里面 hooks、warnings、errors 都是空的,保证调用方总能拿到一份可用结果。

调用关系:它接在 fetch_hooks_list 后面用,负责把“大包结果”整理成“当前目录的小结果”。hooks 加载完成后的界面刷新,以及启动时的 hooks 复查流程,都会用它来得到当前目录该显示什么。

调用图:被 2 处调用(on_hooks_loaded, load_startup_hooks_review_entry)。

hook_needs_review51–56 ↗
fn hook_needs_review(hook: &HookMetadata) -> bool

作用:判断一个 hook 是否需要用户重新看一眼。只有不可信或已经被修改过的 hook,才需要复查。

数据流:进去的是一个 hook 的元信息;它读取其中的 trust_status,也就是信任状态;如果状态是 Untrusted(未信任)或 Modified(已修改),就返回 true,否则返回 false,不会改动任何数据。

调用关系:它是界面做按钮和批量操作判断的小裁判。用户选择 hook、信任单个 hook、信任全部 hooks 时,相关流程会用它过滤出真正需要处理的项目。

调用图:被 3 处调用(toggle_selected_hook, trust_all_hooks, trust_selected_hook);外部调用 1 个(matches!)。

write_hook_trusts58–92 ↗
async fn write_hook_trusts(
    request_handle: AppServerRequestHandle,
    trust_updates: Vec<HookTrustUpdate>,
) -> Result<ConfigWriteResponse>

作用:把一批 hooks 标记为“已信任”,写进用户配置。用户点击“信任这些 hooks”时,最终会走到这里。

数据流:进去的是后台请求通道,以及多条 HookTrustUpdate,每条包含 hook 的 key 和当前内容哈希值;它把这些更新转换成 JSON 对象,形如给每个 hook 记录 trusted_hash;然后发出 ConfigBatchWrite 请求,把这些内容合并写到 hooks.state 配置位置;出来的是配置写入结果,失败时会返回带上下文说明的错误。

调用关系:它是批量信任 hooks 的核心写入函数。trust_hooks、启动时的 hooks 复查界面,以及 write_hook_trust 都会调用它。它不直接碰配置文件,而是把写入任务交给 app server 的 request_typed,让后台统一处理配置写入和重新加载。

调用图:调用 1 个内部函数(request_typed);被 3 处调用(trust_hooks, write_hook_trust, run_startup_hooks_review_app);外部调用 4 个(String, format!, Object, vec!)。

write_hook_trust94–100 ↗
async fn write_hook_trust(
    request_handle: AppServerRequestHandle,
    key: String,
    current_hash: String,
) -> Result<ConfigWriteResponse>

作用:信任单个 hook 的便捷入口。调用方只需要给一个 key 和当前哈希值,不用自己组装列表。

数据流:进去的是后台请求通道、一个 hook 的 key、以及它当前内容的哈希值;它把这一个更新包装成只有一项的列表;然后交给 write_hook_trusts 去执行真正的配置写入;出来的是同样的配置写入结果。

调用关系:它是 write_hook_trusts 的单个版本,方便 trust_hook 这类“只处理一个 hook”的流程使用。真正的通信和写配置逻辑没有重复写,而是复用 write_hook_trusts。

调用图:调用 1 个内部函数(write_hook_trusts);被 1 处调用(trust_hook);外部调用 1 个(vec!)。