第一个文件,今天只学一件事:
你的输入怎样交给下一层?
目标文件是 input_submission.rs。先只看其中从第 108 行开始的提交函数,沿普通文字输入走完五个位置。
你要认出每一步的职责。这层组织和转交请求,模型生成答案的主循环在后面。
它拿到了什么?
fn submit_user_message_with_history_and_shell_escape_policy(
&mut self,
user_message: UserMessage,
history_record: UserMessageHistoryRecord,
shell_escape_policy: ShellEscapePolicy,
) -> (bool, Option<AppCommand>) {
fn 标记函数开头。括号里列出这段代码工作时要用的东西,今天重点看 user_message:本次用户输入。它已经是整理好的数据,不是原始键盘按键。
第一遍只要能说:“它接收本次输入,并处理提交”,就可以往下走。
其他参数与返回值,现在可以怎么读?
&mut self:当前聊天组件,代码可以更新它的状态;借用规则暂时跳过。history_record:本次输入怎样记入输入历史,不是把整个 Thread 的历史传进来。shell_escape_policy:是否允许把 ! 开头的输入走用户命令通道;今天不走这个分支。(bool, Option<AppCommand>):返回两个信息——这一层是否接受/处理了输入,以及有没有生成的命令。它不返回模型的最终答案;排队路径也可能返回(true, None)。
源码:函数入口 · 上游怎样组成 UserMessage
是不是每次输入都立刻往下交?
不是。配置尚未准备好时,会先排队;文字和两类图片都没有时,会直接返回。
if user_message.text.is_empty()
&& user_message.local_images.is_empty()
&& user_message.remote_image_urls.is_empty()
{
return (false, None);
}
if 是“如果”;is_empty() 是“是否为空”;&& 是“同时成立”;return 是“结束当前函数并交回结果”。
这段检查的是“文字和图片都没有”,不是“没有文字就一定拒绝”。先学会沿着条件判断走流程。
校正:为什么 Codex 桌面端看起来可以“空白发送”?
因为本页读的是终端 TUI 的 ChatWidget,不是 Codex 桌面客户端自己的前端。桌面端不会调用这里的 submit_user_message...,所以不能拿这段 TUI 的界面检查直接推断桌面端按钮是否可点。
而且“编辑框没有可见文字”不等于“传给 Core 的整个 UserInput 列表为空”。即使列表真的为空,Core 对“空闲 Thread 启动新 Turn”和“向活动 Turn 追加输入”的处理也不同。完整分层规则见第 3 站:四种“空”。
你的文字怎样变成程序能统一处理的内容?
let mut items: Vec<UserInput> = Vec::new();
// 中间省略其他分支;下面是普通文字处理
if !text.is_empty() {
items.push(UserInput::Text {
text: text.clone(),
text_elements: app_server_text_elements(&text_elements),
});
}
先准备一个叫 items 的空列表。text.is_empty() 问的是“文字是不是空的”;前面的 ! 把结果反过来,所以 if !text.is_empty() 是“如果文字不为空”。
text 来自本次传进来的 user_message。text.clone() 复制的是本次的文字,不会读取或补入上次输入。items.push(...) 是把包装好的文字输入加进本次列表;真正交出去要等后面的 submit_op。
items 装的是本次提交的输入内容,不是整段 Thread 的历史,也不是未来生成的回答。
小检验:上次输入“检查代码”,这次输入“补测试”,复制哪一句?
复制“补测试”,因为这里的 text 是本次输入。如果本次文字为空,这段 if 就不添加文字项;它不会拿“检查代码”来补。能否提交还要看其他检查,例如本次有没有图片。
今天需要懂 Vec 和 text_elements 吗?
Vec<UserInput> 先读成“装用户输入的列表”;let mut 先读成“建立一个可以改变的变量”。文字元素转换和 Rust 如何管理复制后的数据,暂时不用展开。
为什么不能只交出那一句文字?
同样一句“检查代码”,还需要知道在哪个目录、用哪个模型、按哪些权限工作。AppCommand::user_turn(...) 把输入和这些设置组成一条内部命令,起名为 op。
| 你在代码里找 | 先这样理解 |
|---|---|
items | 本次要处理的内容 |
self.config.cwd | 工作目录:在哪个项目里做事 |
approval_policy、active_permission_profile | 审批与权限设置 |
effective_mode.model() | 选用的模型 |
reasoning_effort() | 推理强度设置 |
这里是在组装内部命令,不是在生成答案,也还不是把 JSON-RPC 文本直接写到网络上。
源码:组装命令 · user_turn 的实现
哪里把东西真正交出去了?
if !self.submit_op(op.clone()) {
return (false, None);
}
submit_op 将内部命令交到后续处理路径。若这一步没成功,当前函数就停止;成功提交也只表示这一层已交接,不表示模型已经完成工作。
submit_op 仍然在 TUI 内部:调用它的是 TUI 的 ChatWidget。在当前 AppEvent 路径中,它把 op 包装成 AppEvent::CodexOp,交给 TUI 自己的事件处理器;事件处理器随后才调用 App Server 的 turn/start 接口。
这里不是“submit_op 离开后又回到 TUI”,而是“同一个 TUI 内部,从 ChatWidget 子组件向上交给应用协调层”。跨到 App Server 的边界发生在后面的 turn_start 调用。
把交接后的主链路展开
先这样读:TUI / ChatWidget 的 submit_op → TUI / AppEvent::CodexOp → TUI / 事件处理器 → App Server 的 turn/start → Core 的 start_or_steer_turn。前三步都还在 TUI 内部;App Server 会把界面使用的输入格式转换成 Core 使用的输入格式,再交给 Core 启动或追加一轮工作。
这里的“消息类型”先读成“消息的分类标签”。程序收到消息后,会根据标签决定用哪段代码处理。在当前代码中,可以先把 :: 读成“某个大类中的一种”:AppEvent::CodexOp 是 AppEvent 大类中的 CodexOp 消息;ServerNotification::TurnCompleted 是 ServerNotification 大类中的 TurnCompleted 通知。
| 名字 | 所在层 | 作用 |
|---|---|---|
AppEvent::CodexOp | TUI | ChatWidget 向 TUI 应用协调层发送内部消息 |
turn/start | App Server 接口 | TUI 请求 App Server 启动或追加一轮工作 |
| Core 事件 | Core | 描述执行中的文字、工具和状态变化 |
ServerNotification | App Server 协议 | 把过程和完成状态通知给 TUI |
Core 执行期间不是最后一次性返回一个结果,而是不断产生事件;App Server 将这些事件转换成通知,TUI 边接收边更新文字、工具调用和状态。TUI 收到 turn/completed(代码中的 TurnCompleted)时,才知道这轮已经结束。
日常使用中,如果回答正在逐字出现,或者工具仍在运行,可以先判断为“本次 Turn 正在进行”,对应状态 InProgress;眼前出现的内容是过程事件,不是最终完成信号。
“结束”还不一定等于“成功”:通知里的状态可能是 Completed、Failed 或 Interrupted。只有 Completed 表示本次 Turn 成功完成;整个 Thread 仍然保留,以后还能追加新的 Turn。
继续追踪:ChatWidget.submit_op · TUI 的 AppEvent 定义 · TUI 接住内部命令 · 调用 turn/start · App Server 处理请求 · 交给 Core · 发出完成通知 · TUI 接收通知 · 通知名称映射 · Turn 状态
后面第 401 行起的 history 是什么?
这里会保存输入历史,便于以后重新调出曾经输入的内容;周围还处理用户消息显示和待提交状态。这些界面/输入历史工作,与整个 Thread 的持久化对话历史不是同一层概念。
在特定路径中,用户消息会先显示再提交,所以“界面出现了我的消息”也不能单独证明模型已经开始执行。
源码:提交位置 · submit_op 实现 · 后续 turn/start · 输入历史
第一遍明确跳过什么?
图片细节、Skill / Plugin / App 提及、! 命令、执行中追加输入、界面重绘和 Rust 借用/泛型细节。只要知道“这些是特殊输入或特殊状态的分支”,就继续看主线。
之后再分别学习这些分支,并不把它们当成已经理解。
怎样算这一课通过?
能找到上面五个位置,并解释“输入怎样被检查、整理、带上设置并提交”,就达到今天的目标。
练习 1:用自己的话解释 items.push(UserInput::Text { ... }),再展开参考答案。
把本次输入的文字整理成“文字类型的输入项”,追加到本次待提交的内容列表中。
练习 2:只有一张图片、没有文字,会被第 128 行的检查拒绝吗?先假设配置已准备好、模型支持图片。
不会因为“文字和图片全空”这条检查被拒绝:有图片,所以三个空条件不会同时成立。其他检查是另外的判断。
练习 3:submit_op 返回 true,能说明用户要求已经完成吗?
不能。它表示这里的提交路径通过了,不是后台 Turn 的完成结果。模型执行和进度回传是后续阶段。