Codex 系统手册

Chat widget 交互与命令流程

stage-10.1.323 个文件

这一阶段是终端聊天界面的“前台大厅”,属于系统主干活儿的循环。chatwidget 主控台接住后台消息和用户按键;interaction 判断按键该交给输入框、弹窗还是任务;input_submission、input_flow、input_queue、input_restore 负责发送、排队和找回未发内容。slash_dispatch 分发 / 命令。protocol 把后台通知变成聊天记录、进度、报错。各种菜单和弹窗处理模型、用量、审查、目标、技能、连接器;rendering 最后把这些拼成完整画面。

本阶段的文件23

控件核心和渲染

定义主聊天控件状态,渲染其 UI,并处理延迟的中断式事件和通知。

tui/src/chatwidget.rs源码 ↗
orchestrationmain loop / request handling / rendering

可以把这个文件理解成“终端聊天室的前台经理”。后台真正跑模型、执行命令、查文件;这里不直接干那些重活,而是决定界面该显示什么、什么时候刷新、用户按键该变成什么请求。它维护已固定的聊天记录,也维护正在变化的“当前输出”,比如模型边说边显示、命令边跑边吐日志。它还管底部输入区、审批弹窗、反馈窗口、记忆设置、插件提及、MCP 工具状态、token 用量、退出和中断等。重要的是,它把很多零散状态同步起来:比如任务在跑时底部要转圈,活动单元变化时转录浮层要更新,窗口变宽变窄时流式文本要重新换行。没有它,后台事件和用户操作就会散成一地,终端界面会不知道该显示、刷新或提交什么。

函数细节112
queued_message_edit_binding_for_terminal217–241 ↗
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding

作用:根据用户正在用的终端,挑一个“把队列里上一条消息拿回来编辑”的快捷键。有些终端会拦截 Alt+Up,所以这里会换成更可靠的 Shift+Left。

数据流:输入是一份终端信息,包括终端名字和是否在 tmux 这类复用器里 → 它判断哪些按键可能被吞掉 → 输出一个适合当前环境的快捷键。

调用关系:它是快捷键选择的底层规则,queued_message_edit_hint_binding 会先调用它,再决定界面上实际显示哪个提示键。

调用图:调用 2 个内部函数(alt, shift);被 1 处调用(queued_message_edit_hint_binding);外部调用 1 个(matches!)。

queued_message_edit_hint_binding243–252 ↗
fn queued_message_edit_hint_binding(
    bindings: &[KeyBinding],
    terminal_info: TerminalInfo,
) -> Option<KeyBinding>

作用:从已配置的快捷键列表里,挑一个最适合显示给用户看的提示键。它优先用当前终端最可靠的那个键。

数据流:输入是用户配置的快捷键列表和终端信息 → 它先算出终端推荐键,再看配置里有没有 → 输出推荐键;如果没有,就退回列表第一个。

调用关系:它调用 queued_message_edit_binding_for_terminal 做终端判断,结果用于聊天界面的快捷键提示。

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

normalize_thread_name254–257 ↗
fn normalize_thread_name(name: &str) -> Option<String>

作用:把会话名整理成可保存的名字。空白名字会被当成没有名字。

数据流:输入是一段名字文本 → 它去掉前后空格并检查是否为空 → 输出整理后的字符串,或在空时输出 None。

调用关系:它是会话命名的小工具,供需要保存或显示会话名的流程使用。

contains_plan_keyword814–817 ↗
fn contains_plan_keyword(text: &str) -> bool

作用:判断一段文字里有没有独立的单词“plan”。这用于决定是否提示用户进入计划模式,而不是误把 planning 这类词也算进去。

数据流:输入是一段用户草稿 → 它按非字母数字和下划线切词,再逐个忽略大小写比较 → 输出 true 或 false。

调用关系:它是计划模式提示的词法判断工具,其他刷新提示的逻辑会用它来决定要不要出现提醒。

ThreadItemRenderSource::is_replay826–828 ↗
fn is_replay(self) -> bool

作用:判断当前要渲染的线程项目是不是从历史回放来的。实时消息和恢复旧会话时的消息需要区别对待。

数据流:输入是一个渲染来源枚举 → 它检查是否属于 Replay → 输出布尔值。

调用关系:handle_thread_item 会用它判断消息是直播还是回放,从而避免重复记录或触发不该触发的实时行为。

调用图:被 1 处调用(handle_thread_item);外部调用 1 个(matches!)。

ThreadItemRenderSource::replay_kind830–835 ↗
fn replay_kind(self) -> Option<ReplayKind>

作用:如果当前项目是历史回放,它告诉调用方是哪一种回放。不是回放就返回没有。

数据流:输入是渲染来源 → 它拆开 Replay 里的具体类型 → 输出回放类型或 None。

调用关系:handle_thread_item 会用它细分恢复初始消息和线程快照等场景。

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

exec_approval_request_from_params838–859 ↗
fn exec_approval_request_from_params(
    params: CommandExecutionRequestApprovalParams,
    fallback_cwd: &AbsolutePathBuf,
) -> ExecApprovalRequestEvent

作用:把服务器发来的“要执行命令,需不需要批准”的参数,转换成界面内部能显示和处理的审批请求。

数据流:输入是服务器参数和备用工作目录 → 它拆出命令、目录、理由、权限和审批编号;缺目录时用备用目录 → 输出 ExecApprovalRequestEvent。

调用关系:这是协议转换层的小桥梁,后续底部审批窗口会拿这个事件展示给用户。

patch_approval_request_from_params861–871 ↗
fn patch_approval_request_from_params(
    params: FileChangeRequestApprovalParams,
) -> ApplyPatchApprovalRequestEvent

作用:把服务器发来的“要改文件,需不需要批准”的参数,转换成界面内部的补丁审批请求。

数据流:输入是文件变更审批参数 → 它保留调用编号、轮次编号、理由和授权根目录,并先放一个空的变更表 → 输出 ApplyPatchApprovalRequestEvent。

调用关系:它服务于文件修改审批流程,生成的事件会被审批 UI 使用。

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

request_permissions_from_params873–885 ↗
fn request_permissions_from_params(
    params: codex_app_server_protocol::PermissionsRequestApprovalParams,
) -> std::io::Result<RequestPermissionsEvent>

作用:把服务器的权限申请参数转换成核心协议里的权限请求。转换可能失败,所以返回结果类型。

数据流:输入是服务器权限参数 → 它复制轮次、调用、环境、时间、理由、目录,并把权限列表尝试转成内部格式 → 成功输出 RequestPermissionsEvent,失败输出错误。

调用关系:它连接服务器协议和本地审批/权限界面,让用户能看懂并决定是否授权。

token_usage_info_from_app_server887–905 ↗
fn token_usage_info_from_app_server(token_usage: ThreadTokenUsage) -> TokenUsageInfo

作用:把服务器统计的 token 用量转换成 TUI 自己使用的用量结构。token 可以理解成模型处理文字的计量单位。

数据流:输入是服务器返回的总用量、最近一次用量和上下文窗口大小 → 它逐项拷贝到内部 TokenUsageInfo → 输出可用于状态栏显示的数据。

调用关系:token 用量事件到来时会用这个转换结果,再交给 set_token_info 更新底部状态。

ChatWidget::set_collab_agent_metadata914–927 ↗
fn set_collab_agent_metadata(
        &mut self,
        thread_id: ThreadId,
        agent_nickname: Option<String>,
        agent_role: Option<String>,
    )

作用:保存某个协作子代理线程的昵称和角色。这样界面显示通知时不会只露出一串生硬的线程 ID。

数据流:输入是线程 ID、可选昵称、可选角色 → 它写入 collab_agent_metadata 这张缓存表 → 之后渲染相关消息时能读到友好名字。

调用关系:replace_chat_widget 等外层流程会在切换或恢复聊天界面时调用它,确保后续通知能正确显示子代理身份。

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

ChatWidget::collab_agent_metadata930–935 ↗
fn collab_agent_metadata(&self, thread_id: ThreadId) -> AgentMetadata

作用:读取某个协作子代理的显示资料。没有登记过时,它返回一份空资料,避免界面崩掉。

数据流:输入是线程 ID → 它查缓存表 → 输出找到的 AgentMetadata,或默认空值。

调用关系:这是协作代理通知和渲染时的查询入口,供本文件其他处理流程使用。

ChatWidget::restore_retry_status_header_if_present937–941 ↗
fn restore_retry_status_header_if_present(&mut self)

作用:如果之前因为重试保存过状态标题,就把它恢复到界面上。这样重试结束后状态栏不会丢失上下文。

数据流:它从 status_state 取出暂存的重试标题 → 如果存在,就设置为当前状态标题 → 暂存区被清空。

调用关系:它调用 take_retry_status_header,属于重试流程结束后的 UI 修复步骤。

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

ChatWidget::record_agent_markdown944–948 ↗
fn record_agent_markdown(&mut self, message: &str)

作用:记录模型当前轮次产生的原始 Markdown 文本。Markdown 是一种用符号表示标题、列表、粗体的纯文本格式。

数据流:输入是一段模型输出 → 如果不为空,就写入 transcript 的模型 Markdown 记录 → 没有直接返回值。

调用关系:它把可见输出之外的原文留给转录、复制或后续分析使用。

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

ChatWidget::record_visible_user_turn_for_copy950–952 ↗
fn record_visible_user_turn_for_copy(&mut self)

作用:标记当前有一条用户消息出现在对话里,方便“复制最近对话”这类功能知道边界。

数据流:它不接收外部数据 → 通知 transcript 记录一个可见用户轮次 → 更新复制相关的内部状态。

调用关系:on_user_message_display 在真正把用户消息放进历史时会调用它。

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

ChatWidget::open_feedback_note954–960 ↗
fn open_feedback_note(
        &mut self,
        category: crate::app_event::FeedbackCategory,
        include_logs: bool,
    )

作用:打开反馈备注输入框,让用户给当前问题补充说明。

数据流:输入是反馈类别和是否带日志 → 它转交给 show_feedback_note → 界面底部出现反馈视图。

调用关系:这是对外公开的小入口,实际创建视图和刷新界面的工作由 show_feedback_note 完成。

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

ChatWidget::show_feedback_note962–975 ↗
fn show_feedback_note(
        &mut self,
        category: crate::app_event::FeedbackCategory,
        include_logs: bool,
    )

作用:真正创建并显示反馈备注窗口。它会把当前轮次 ID 和事件发送器交给这个窗口,方便提交反馈。

数据流:输入是反馈类别和是否包含日志 → 它创建 FeedbackNoteView,放到底部面板显示 → 请求重绘屏幕。

调用关系:open_feedback_note 调用它;它通过 bottom_pane.show_view 把工作交给底部面板。

调用图:调用 3 个内部函数(show_view, new, request_redraw);被 1 处调用(open_feedback_note);外部调用 2 个(new, clone)。

ChatWidget::dismiss_app_server_request987–995 ↗
fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest)

作用:移除一个已经被远端解决的服务器请求,避免用户还能在本地继续点它。

数据流:输入是已解析的服务器请求 → 它分别从延迟队列和当前底部面板中移除 → 如果确实移除了东西,就刷新界面。

调用关系:它连接服务器请求状态和本地弹窗状态,调用 interrupts.remove_resolved_prompt 和 bottom_pane.dismiss_app_server_request。

调用图:调用 3 个内部函数(dismiss_app_server_request, request_redraw, remove_resolved_prompt)。

ChatWidget::open_multi_agent_enable_prompt1018–1051 ↗
fn open_multi_agent_enable_prompt(&mut self)

作用:当用户想用子代理但配置没开时,弹出“要不要启用”的选择框。

数据流:它构造“启用”和“暂不”两个选项 → 启用选项会发送更新特性开关事件并插入提示历史 → 底部面板显示选择窗口。

调用关系:它是协作/子代理功能的引导入口,选择后的实际配置更新通过 AppEvent 交给外层处理。

调用图:调用 2 个内部函数(show_selection_view, standard_popup_hint_line);外部调用 2 个(default, vec!)。

ChatWidget::open_memories_popup1053–1066 ↗
fn open_memories_popup(&mut self)

作用:打开记忆功能设置窗口;如果记忆功能还没启用,就先弹出启用提示。

数据流:它读取配置里的 MemoryTool 开关 → 未启用时调用 open_memories_enable_prompt;已启用时创建 MemoriesSettingsView → 放到底部面板。

调用关系:它是记忆设置的主入口,必要时把流程转给 open_memories_enable_prompt。

调用图:调用 4 个内部函数(list_keymap, show_view, new, open_memories_enable_prompt);外部调用 2 个(new, clone)。

ChatWidget::open_memories_enable_prompt1068–1102 ↗
fn open_memories_enable_prompt(&mut self)

作用:弹出“是否启用记忆”的选择窗口。记忆指系统可在会话间保存一些偏好或信息。

数据流:它构造启用和暂不两个选项,并附上文档链接 → 用户选启用时发送更新特性开关事件 → 底部面板显示这个选择框。

调用关系:open_memories_popup 在发现功能未开时会调用它。

调用图:调用 2 个内部函数(show_selection_view, standard_popup_hint_line);被 1 处调用(open_memories_popup);外部调用 3 个(default, from, vec!)。

ChatWidget::set_memory_settings1104–1107 ↗
fn set_memory_settings(&mut self, use_memories: bool, generate_memories: bool)

作用:更新当前界面里的记忆使用设置。它只改本地配置状态,方便界面立即反映用户选择。

数据流:输入是是否使用记忆、是否生成记忆 → 它写入 config.memories 对应字段 → 没有返回值。

调用关系:记忆设置视图确认后会用它同步 ChatWidget 内的配置副本。

ChatWidget::set_token_info1109–1118 ↗
fn set_token_info(&mut self, info: Option<TokenUsageInfo>)

作用:更新底部显示的 token 用量信息。没有信息时就清空显示。

数据流:输入是可选 TokenUsageInfo → 有值时交给 apply_token_info 计算并显示;无值时把上下文窗口显示清空并清掉缓存。

调用关系:handle_token_count 会调用它;它再把具体计算交给 apply_token_info。

调用图:调用 2 个内部函数(set_context_window, apply_token_info);被 1 处调用(handle_token_count)。

ChatWidget::apply_token_info1120–1125 ↗
fn apply_token_info(&mut self, info: TokenUsageInfo)

作用:把 token 用量换算成用户能看的状态,比如上下文还剩百分之多少。

数据流:输入是 TokenUsageInfo → 它计算剩余百分比和已用数量 → 更新底部面板,再保存这份用量信息。

调用关系:set_token_info 和 restore_pre_review_token_info 会调用它,它依赖 context_remaining_percent 和 context_used_tokens。

调用图:调用 3 个内部函数(set_context_window, context_remaining_percent, context_used_tokens);被 2 处调用(restore_pre_review_token_info, set_token_info)。

ChatWidget::context_remaining_percent1127–1132 ↗
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64>

作用:计算模型上下文窗口还剩多少百分比。上下文窗口可以理解成模型一次能记住和处理的文字容量。

数据流:输入是 token 用量信息 → 如果知道窗口大小,就用最近一次用量计算剩余百分比 → 输出可选百分比。

调用关系:apply_token_info 调用它,用于决定底部状态显示百分比还是已用 token 数。

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

ChatWidget::context_used_tokens1134–1140 ↗
fn context_used_tokens(&self, info: &TokenUsageInfo, percent_known: bool) -> Option<i64>

作用:在无法显示百分比时,改为给出已经占用的 token 数。

数据流:输入是用量信息和是否已知道百分比 → 如果百分比已知,输出 None;否则输出当前上下文里的 token 数。

调用关系:apply_token_info 调用它,作为上下文窗口显示的备用方案。

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

ChatWidget::restore_pre_review_token_info1142–1153 ↗
fn restore_pre_review_token_info(&mut self)

作用:代码审查模式结束后,恢复进入审查前的 token 显示。这样临时审查不会污染原会话状态。

数据流:它取出 review 中保存的旧 token 信息 → 有旧值就重新应用;旧值是 None 就清空底部显示 → 同时清掉暂存。

调用关系:exit_review_mode_after_item 在退出审查模式时调用它。

调用图:调用 2 个内部函数(set_context_window, apply_token_info);被 1 处调用(exit_review_mode_after_item)。

ChatWidget::handle_history_entry_response1155–1163 ↗
fn handle_history_entry_response(&mut self, event: HistoryLookupResponse)

作用:处理历史输入查询的结果,比如用户按上键翻以前发过的话。

数据流:输入包含偏移量、日志 ID 和历史条目 → 它把这些转交给底部输入区 → 输入区更新候选内容。

调用关系:它是历史查询响应进入 UI 的入口,具体输入框行为由 bottom_pane.on_history_entry_response 完成。

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

ChatWidget::pre_draw_tick1165–1182 ↗
fn pre_draw_tick(&mut self)

作用:每次画界面前做一轮小更新,比如动画、提示、宠物、标题状态等。它像“开画前巡检”。

数据流:它读取当前各种 UI 状态 → 更新钩子可见性、底部面板、宠物帧、计划提示、目标状态和终端标题 → 必要时安排后续刷新。

调用关系:handle_tui_event、关机反馈和钩子显隐流程会在绘制前调用它。

调用图:调用 1 个内部函数(pre_draw_tick);被 5 处调用(handle_tui_event, show_shutdown_feedback, expire_quiet_hook_linger, reveal_running_hooks, reveal_running_hooks_after_delayed_redraw)。

ChatWidget::flush_active_cell1184–1190 ↗
fn flush_active_cell(&mut self)

作用:把正在变化的活动消息固定进正式聊天历史。比如流式输出结束后,就不能再当临时内容改来改去了。

数据流:它从 transcript 取走 active_cell → 设置后续分隔标记 → 发送 InsertHistoryCell 事件插入历史,并请求插入待显示的用量信息。

调用关系:add_boxed_history、add_mcp_output、apply_session_info_cell、exit_review_mode_after_item 等在切换历史块前会调用它。

调用图:调用 1 个内部函数(send);被 4 处调用(add_boxed_history, add_mcp_output, apply_session_info_cell, exit_review_mode_after_item);外部调用 1 个(InsertHistoryCell)。

ChatWidget::add_to_history1192–1194 ↗
fn add_to_history(&mut self, cell: impl HistoryCell + 'static)

作用:往聊天记录里加一条新的历史单元。历史单元可以是消息、警告、错误、状态行等。

数据流:输入是一个具体的 HistoryCell → 它装箱成统一类型 → 交给 add_boxed_history 处理插入细节。

调用关系:很多公开的 add_* 方法会调用它,比如添加错误、提示、调试配置和审查状态。

调用图:调用 1 个内部函数(add_boxed_history);被 9 处调用(add_debug_config_output, add_error_message, add_info_message, add_memories_enable_notice, add_ps_output, add_warning_message, enter_review_mode_with_hint, exit_review_mode_after_item, on_user_message_display);外部调用 1 个(new)。

ChatWidget::add_boxed_history1196–1217 ↗
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>)

作用:真正把一条已装箱的历史单元放进记录,同时处理活动单元、分隔线和流式输出边界。

数据流:输入是一条 Box<dyn HistoryCell> → 它判断是否要先冲刷活动单元、是否记录可见活动、是否保留占位会话头 → 最后发送 InsertHistoryCell。

调用关系:add_to_history、add_plain_history_lines、apply_session_info_cell、finalize_active_cell_as_failed 都会用它做统一插入。

调用图:调用 2 个内部函数(send, flush_active_cell);被 4 处调用(add_plain_history_lines, add_to_history, apply_session_info_cell, finalize_active_cell_as_failed);外部调用 1 个(InsertHistoryCell)。

ChatWidget::enter_review_mode_with_hint1219–1230 ↗
fn enter_review_mode_with_hint(&mut self, hint: String, from_replay: bool)

作用:进入代码审查模式,并在聊天里显示一条“审查开始”的提示。

数据流:输入是提示文字和是否来自回放 → 它保存进入前的 token 信息,必要时把任务状态设为运行中,打开 review 标记 → 插入审查开始状态行并刷新。

调用关系:审查流程启动时调用它;它通过 add_to_history 写入可见提示。

调用图:调用 4 个内部函数(is_task_running, set_task_running, add_to_history, request_redraw);外部调用 2 个(format!, new_review_status_line)。

ChatWidget::exit_review_mode_after_item1232–1242 ↗
fn exit_review_mode_after_item(&mut self)

作用:结束代码审查模式,并恢复正常聊天状态。

数据流:它先冲刷答案流、待处理中断和活动单元 → 关闭 review 标记,恢复旧 token 信息 → 插入“审查结束”状态行并刷新。

调用关系:审查项目处理完后调用它,内部会用 restore_pre_review_token_info 和 flush_active_cell 收尾。

调用图:调用 4 个内部函数(add_to_history, flush_active_cell, request_redraw, restore_pre_review_token_info);外部调用 1 个(new_review_status_line)。

ChatWidget::on_committed_user_message1244–1342 ↗
fn on_committed_user_message(&mut self, items: &[UserInput], from_replay: bool)

作用:当一条用户消息被确认进入线程时,决定要不要把它显示到聊天记录和历史输入里。

数据流:输入是用户输入项目列表和是否来自回放 → 回放时重建提及绑定并记录到输入历史;实时消息时尝试匹配待发送队列,避免重复显示 → 最后可能调用 on_user_message_display。

调用关系:线程项目处理流程会调用它;它把真正渲染用户消息的事交给 on_user_message_display。

调用图:调用 3 个内部函数(record_replayed_user_message_history, on_user_message_display, user_message_display_for_history);外部调用 5 个(pending_steer_compare_key_from_items, user_message_display_from_inputs, new, iter, warn!)。

ChatWidget::on_user_message_display1344–1362 ↗
fn on_user_message_display(&mut self, display: UserMessageDisplay)

作用:把用户消息真正显示成聊天记录里的一个气泡,并记录复制边界。

数据流:输入是 UserMessageDisplay → 它保存最近显示内容;如果文本、图片或富文本不为空,就记录用户轮次并插入用户提示历史单元 → 重置后续分隔状态。

调用关系:on_committed_user_message 会调用它;它又调用 record_visible_user_turn_for_copy 和 add_to_history。

调用图:调用 2 个内部函数(add_to_history, record_visible_user_turn_for_copy);被 1 处调用(on_committed_user_message);外部调用 2 个(new_user_prompt, clone)。

ChatWidget::request_immediate_exit1368–1370 ↗
fn request_immediate_exit(&self)

作用:要求程序立刻退出,不等待后台优雅关闭。通常用于紧急或关机已完成的情况。

数据流:它不接收输入 → 发送 Exit(Immediate) 事件 → 外层应用收到后立即结束界面。

调用关系:它是退出流程的低层工具;用户主动退出通常更偏向 request_quit_without_confirmation。

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

ChatWidget::request_quit_without_confirmation1376–1379 ↗
fn request_quit_without_confirmation(&self)

作用:请求退出,但先让后台做关闭清理。用于 /quit、/exit 或双击退出快捷键。

数据流:它不接收输入 → 发送 Exit(ShutdownFirst) 事件 → 外层应用先关后台再退出。

调用关系:它服务用户发起的正常退出流程。

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

ChatWidget::show_shutdown_in_progress1381–1383 ↗
fn show_shutdown_in_progress(&mut self)

作用:在底部面板显示“正在关闭”的状态,让用户知道程序不是卡住了。

数据流:它不接收输入 → 调用 bottom_pane.show_shutdown_in_progress → 底部区域切换为关机提示。

调用关系:show_shutdown_feedback 会调用它,实际显示由底部面板负责。

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

ChatWidget::request_redraw1385–1387 ↗
fn request_redraw(&mut self)

作用:要求终端界面重新画一帧。很多状态变化后都需要它,否则用户看不到最新内容。

数据流:它不接收输入 → 调用 frame_requester.schedule_frame → 下一轮渲染会被安排。

调用关系:大量函数会调用它,比如添加消息、清除加载、调整模式和弹窗变化。

调用图:调用 1 个内部函数(schedule_frame);被 17 处调用(add_diff_in_progress, add_error_message, add_info_message, add_mcp_output, add_memories_enable_notice, add_plain_history_lines, add_warning_message, clear_mcp_inventory_loading, dismiss_app_server_request, enter_review_mode_with_hint (+7 more))。

ChatWidget::bump_active_cell_revision1389–1391 ↗
fn bump_active_cell_revision(&mut self)

作用:给活动单元的版本号加一。版本号像便利贴,告诉转录浮层“这块内容变了,要重新算”。

数据流:它不接收输入 → 调用 transcript.bump_active_cell_revision → 活动单元缓存键发生变化。

调用关系:add_mcp_output 和 clear_mcp_inventory_loading 会在活动内容变化时调用它。

调用图:调用 1 个内部函数(bump_active_cell_revision);被 2 处调用(add_mcp_output, clear_mcp_inventory_loading)。

ChatWidget::finalize_active_cell_as_failed1394–1405 ↗
fn finalize_active_cell_as_failed(&mut self)

作用:把当前正在显示的工具或命令标记为失败,然后固定进聊天历史。

数据流:它取出 active_cell → 如果是命令单元或 MCP 工具单元,就打上失败标记 → 插入历史并请求后续用量输出。

调用关系:当活动工具异常结束时使用它;插入历史由 add_boxed_history 完成。

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

ChatWidget::set_pending_thread_approvals1407–1409 ↗
fn set_pending_thread_approvals(&mut self, threads: Vec<String>)

作用:更新当前还有哪些线程在等待审批。这样底部界面能提醒用户有待处理项。

数据流:输入是线程 ID 字符串列表 → 它转交给 bottom_pane → 底部面板保存并显示这些待审批线程。

调用关系:外层收到审批状态变化后会调用它,具体展示由底部面板处理。

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

ChatWidget::clear_thread_rename_block1411–1413 ↗
fn clear_thread_rename_block(&mut self)

作用:清除阻止重命名线程的提示信息。

数据流:它不接收输入 → 把 thread_rename_block_message 设为 None → 后续重命名界面不再显示旧阻止原因。

调用关系:线程重命名限制解除时使用。

ChatWidget::set_thread_rename_block_message1415–1417 ↗
fn set_thread_rename_block_message(&mut self, message: impl Into<String>)

作用:设置一条说明,告诉用户为什么当前不能重命名线程。

数据流:输入是一段可转成字符串的消息 → 它保存到 thread_rename_block_message → 后续 UI 可以显示这个原因。

调用关系:重命名流程发现被阻止时会调用它。

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

ChatWidget::set_interrupted_turn_notice_mode1419–1421 ↗
fn set_interrupted_turn_notice_mode(&mut self, mode: InterruptedTurnNoticeMode)

作用:设置中断当前轮次后,要不要显示中断提示。

数据流:输入是提示模式 → 它写入 interrupted_turn_notice_mode → 后续中断收尾时按这个模式决定是否提示。

调用关系:中断相关流程会用它调整用户可见反馈。

ChatWidget::add_diff_in_progress1423–1425 ↗
fn add_diff_in_progress(&mut self)

作用:通知界面有 diff 生成或比较正在进行,需要刷新显示。diff 是文件改动对比。

数据流:它不接收输入 → 请求重绘 → 用户界面获得机会显示最新状态。

调用关系:diff 流程开始时调用它,实际刷新由 request_redraw 安排。

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

ChatWidget::on_diff_complete1427–1429 ↗
fn on_diff_complete(&mut self)

作用:通知界面 diff 已经完成,需要再刷新一次。

数据流:它不接收输入 → 请求重绘 → 已完成的 diff 状态能显示出来。

调用关系:diff 流程结束时调用它。

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

ChatWidget::add_debug_config_output1431–1436 ↗
fn add_debug_config_output(&mut self)

作用:把当前调试配置输出到聊天历史里,方便排查设置问题。

数据流:它读取当前 config 和网络代理状态 → 创建调试配置历史单元 → 插入聊天历史。

调用关系:调试命令触发时会用它;创建内容由 debug_config::new_debug_config_output 完成。

调用图:调用 2 个内部函数(add_to_history, new_debug_config_output)。

ChatWidget::add_ps_output1438–1448 ↗
fn add_ps_output(&mut self)

作用:把正在运行的后台终端/命令列表显示到聊天历史里。类似命令行里的 ps 查看进程。

数据流:它读取 unified_exec_processes → 取出命令显示名和最近输出片段 → 创建历史单元并插入。

调用关系:用户查看后台进程时会调用它,展示内容由 history_cell::new_unified_exec_processes_output 构造。

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

ChatWidget::clean_background_terminals1450–1458 ↗
fn clean_background_terminals(&mut self)

作用:停止所有后台终端任务,并在聊天里告诉用户正在停止。

数据流:它提交 clean_background_terminals 命令 → 清空本地后台进程列表并同步底部状态 → 插入一条信息消息。

调用关系:它调用 submit_op 把实际清理请求发给后台,再用 add_info_message 给用户反馈。

调用图:调用 2 个内部函数(add_info_message, submit_op);外部调用 1 个(clean_background_terminals)。

ChatWidget::plugins_for_mentions1460–1466 ↗
fn plugins_for_mentions(&self) -> Option<&[PluginCapabilitySummary]>

作用:返回可用于 @ 提及的插件列表;如果插件功能没开,就返回没有。

数据流:它读取功能开关 → 未启用时输出 None;启用时从底部面板取插件缓存并转成切片 → 输出可选插件列表。

调用关系:提及补全相关逻辑会用它判断哪些插件能被用户输入引用。

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

ChatWidget::placeholder_session_header_cell1469–1482 ↗
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell>

作用:在真正会话信息还没回来前,先生成一个灰色斜体的占位会话头。这样启动时界面不会空白。

数据流:输入是配置 → 它用默认模型名、当前目录、CLI 版本和 yolo 模式状态创建 SessionHeaderHistoryCell → 输出历史单元盒子。

调用关系:会话初始化阶段使用它;真实信息到来后 apply_session_info_cell 会尝试合并替换它。

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

ChatWidget::apply_session_info_cell1485–1510 ↗
fn apply_session_info_cell(&mut self, cell: history_cell::SessionInfoCell)

作用:把真实会话信息放进聊天历史,并尽量替换启动时的占位头,避免显示两个会话头。

数据流:输入是真实 SessionInfoCell → 它检查当前 active_cell 是否是占位会话头;是就替换,不是就恢复原 active_cell → 再冲刷或插入历史。

调用关系:会话配置完成时调用它;它使用 flush_active_cell 和 add_boxed_history 完成历史同步。

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

ChatWidget::add_info_message1512–1515 ↗
fn add_info_message(&mut self, message: String, hint: Option<String>)

作用:往聊天里加一条普通信息提示,比如模式已切换或后台任务已停止。

数据流:输入是消息和可选提示 → 它创建信息历史单元 → 插入历史并请求刷新。

调用关系:clean_background_terminals、set_raw_output_mode_and_notify、toggle_vim_mode_and_notify 等会调用它。

调用图:调用 2 个内部函数(add_to_history, request_redraw);被 3 处调用(clean_background_terminals, set_raw_output_mode_and_notify, toggle_vim_mode_and_notify);外部调用 1 个(new_info_event)。

ChatWidget::add_memories_enable_notice1517–1522 ↗
fn add_memories_enable_notice(&mut self)

作用:显示一条“记忆将在下次会话启用”的提示。

数据流:它使用固定提示文本创建警告历史单元 → 插入历史 → 请求重绘。

调用关系:记忆功能启用流程确认后会用它提醒用户生效时机。

调用图:调用 2 个内部函数(add_to_history, request_redraw);外部调用 1 个(new_warning_event)。

ChatWidget::add_plain_history_lines1524–1527 ↗
fn add_plain_history_lines(&mut self, lines: Vec<Line<'static>>)

作用:把一组已经排好样式的普通文本行加入聊天历史。

数据流:输入是多行 Line → 它包装成 PlainHistoryCell → 插入历史并刷新。

调用关系:需要直接展示现成文本块的地方会调用它。

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

ChatWidget::add_warning_message1529–1532 ↗
fn add_warning_message(&mut self, message: String)

作用:往聊天里加一条警告消息,用来提示用户注意但不一定是致命错误。

数据流:输入是警告文本 → 它创建警告历史单元 → 插入历史并请求刷新。

调用关系:各种风险提示、配置提示会用它。

调用图:调用 2 个内部函数(add_to_history, request_redraw);外部调用 1 个(new_warning_event)。

ChatWidget::add_error_message1534–1537 ↗
fn add_error_message(&mut self, message: String)

作用:往聊天里加一条错误消息,让用户知道某件事失败了。

数据流:输入是错误文本 → 它创建错误历史单元 → 插入历史并刷新。

调用关系:add_app_server_stub_message 会调用它,其他错误处理流程也可使用。

调用图:调用 2 个内部函数(add_to_history, request_redraw);被 1 处调用(add_app_server_stub_message);外部调用 1 个(new_error_event)。

ChatWidget::add_app_server_stub_message1539–1542 ↗
fn add_app_server_stub_message(&mut self, feature: &str)

作用:当某个服务器功能在 TUI 里还没支持时,记录日志并给用户显示“暂不可用”。

数据流:输入是功能名 → 它写一条警告日志 → 拼出错误消息并调用 add_error_message 显示。

调用关系:这是未实现功能的兜底反馈,避免用户操作后完全没反应。

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

ChatWidget::rename_confirmation_cell1544–1554 ↗
fn rename_confirmation_cell(name: &str, thread_id: Option<ThreadId>) -> PlainHistoryCell

作用:生成一条“会话已重命名”的确认消息,并可能附上恢复该会话的命令提示。

数据流:输入是新名字和可选线程 ID → 它组装带颜色的文本行,尝试生成 resume 提示 → 输出 PlainHistoryCell。

调用关系:线程重命名成功后会使用这个历史单元显示确认结果。

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

ChatWidget::add_mcp_output1561–1573 ↗
fn add_mcp_output(&mut self, detail: McpServerStatusDetail)

作用:开始获取 MCP 服务器清单时,在聊天里显示加载动画并向外层发送获取请求。MCP 是一种让模型连接外部工具/服务的协议。

数据流:输入是要查看的服务器状态细节 → 它先冲刷答案流和活动单元,再放入 MCP 加载单元、更新版本并刷新 → 发送 FetchMcpInventory 事件。

调用关系:用户查看 MCP 信息时调用它;结果回来后 clear_mcp_inventory_loading 会移除加载动画。

调用图:调用 5 个内部函数(send, bump_active_cell_revision, flush_active_cell, request_redraw, thread_id);外部调用 2 个(new, new_mcp_inventory_loading)。

ChatWidget::clear_mcp_inventory_loading1579–1592 ↗
fn clear_mcp_inventory_loading(&mut self)

作用:清掉 MCP 清单加载动画,但只在当前活动单元确实还是那个加载动画时才清。

数据流:它检查 active_cell → 如果不是 McpInventoryLoadingCell 就不动;如果是,就清空 active_cell、更新版本并刷新。

调用关系:MCP 清单结果到达时调用它,谨慎检查类型是为了避免误删别的正在显示的内容。

调用图:调用 2 个内部函数(bump_active_cell_revision, request_redraw)。

ChatWidget::apply_file_search_result1595–1597 ↗
fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>)

作用:把文件搜索结果交给底部输入区,用于 @ 文件补全或类似搜索展示。

数据流:输入是查询字符串和匹配文件列表 → 它转交给 bottom_pane.on_file_search_result → 底部面板更新搜索候选。

调用关系:文件搜索异步结果返回时调用它。

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

ChatWidget::current_stream_width1604–1614 ↗
fn current_stream_width(&self, reserved_cols: usize) -> Option<usize>

作用:计算当前流式输出正文能用的宽度。因为历史单元旁边可能有项目符号、缩进或边距,正文不能直接用整个终端宽度。

数据流:输入是外层包装要占掉的列数 → 它读取上次渲染宽度,扣掉历史换行和保留列 → 输出可用正文宽度,未知时输出 None。

调用关系:on_terminal_resize 会调用它来更新普通回答流和计划流的换行宽度。

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

ChatWidget::raw_output_mode1616–1618 ↗
fn raw_output_mode(&self) -> bool

作用:告诉调用方当前是否处在纯文本输出模式。纯文本模式更方便在终端里选择复制。

数据流:它读取 raw_output_mode 字段 → 输出 true 或 false。

调用关系:渲染或设置界面会用它判断当前显示模式。

ChatWidget::history_render_mode1620–1626 ↗
fn history_render_mode(&self) -> HistoryRenderMode

作用:根据当前设置决定历史记录用富样式渲染还是原始文本渲染。

数据流:它读取 raw_output_mode → 开启时输出 Raw,否则输出 Rich。

调用关系:set_raw_output_mode 会调用它,并把结果同步给流式输出控制器。

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

ChatWidget::set_raw_output_mode1628–1639 ↗
fn set_raw_output_mode(&mut self, enabled: bool)

作用:切换纯文本输出模式,并同步给配置、流式控制器和状态区域。

数据流:输入是是否启用 → 它更新字段和配置,计算新的渲染模式,通知普通流和计划流控制器 → 刷新状态表面。

调用关系:set_raw_output_mode_and_notify 调用它做实际切换。

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

ChatWidget::raw_output_mode_notice1641–1647 ↗
fn raw_output_mode_notice(enabled: bool) -> &'static str

作用:返回切换纯文本模式后要显示给用户的说明文字。

数据流:输入是是否启用 → 它选择“已开启”或“已关闭”的固定提示 → 输出静态字符串。

调用关系:set_raw_output_mode_and_notify 会用它生成聊天里的通知。

ChatWidget::set_raw_output_mode_and_notify1649–1655 ↗
fn set_raw_output_mode_and_notify(&mut self, enabled: bool)

作用:切换纯文本输出模式,并在聊天里告诉用户切换结果。

数据流:输入是是否启用 → 它调用 set_raw_output_mode 修改状态 → 调用 raw_output_mode_notice 取提示,再用 add_info_message 显示。

调用关系:toggle_raw_output_mode_and_notify 会调用它完成实际切换和反馈。

调用图:调用 2 个内部函数(add_info_message, set_raw_output_mode);被 1 处调用(toggle_raw_output_mode_and_notify);外部调用 1 个(raw_output_mode_notice)。

ChatWidget::toggle_raw_output_mode_and_notify1657–1661 ↗
fn toggle_raw_output_mode_and_notify(&mut self) -> bool

作用:把纯文本输出模式从开切到关,或从关切到开,并返回新状态。

数据流:它读取当前 raw_output_mode 后取反 → 调用 set_raw_output_mode_and_notify → 输出切换后的布尔值。

调用关系:用户按快捷键或命令切换显示模式时会用它。

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

ChatWidget::on_terminal_resize1667–1682 ↗
fn on_terminal_resize(&mut self, width: u16)

作用:终端宽度变化后,更新流式文本的换行宽度,并在第一次知道宽度时刷新界面。

数据流:输入是新宽度 → 它保存宽度,分别计算回答流和计划流可用宽度,更新控制器 → 同步活动流尾巴,必要时请求重绘。

调用关系:终端 resize 事件触发时调用它;它依赖 current_stream_width。

调用图:调用 2 个内部函数(current_stream_width, request_redraw)。

ChatWidget::has_active_agent_stream1685–1687 ↗
fn has_active_agent_stream(&self) -> bool

作用:判断当前是否有模型回答正在流式输出。

数据流:它检查 stream_controller 是否存在 → 输出 true 或 false。

调用关系:其他流程用它判断能否插入、打断或等待普通回答流。

ChatWidget::has_active_plan_stream1690–1692 ↗
fn has_active_plan_stream(&self) -> bool

作用:判断当前是否有计划内容正在流式输出。

数据流:它检查 plan_stream_controller 是否存在 → 输出 true 或 false。

调用关系:计划模式相关 UI 和打断逻辑会使用它。

ChatWidget::is_plan_streaming_in_tui1694–1696 ↗
fn is_plan_streaming_in_tui(&self) -> bool

作用:内部判断 TUI 里是否正在显示计划流。

数据流:它读取 plan_stream_controller → 有控制器输出 true,否则 false。

调用关系:本文件内部的计划流处理会用它。

ChatWidget::composer_is_empty1698–1700 ↗
fn composer_is_empty(&self) -> bool

作用:询问底部输入框当前是不是空的。

数据流:它不直接检查文本,而是调用 bottom_pane.composer_is_empty → 输出布尔值。

调用关系:提交、退出、快捷键处理等流程会用它判断用户是否有未发送草稿。

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

ChatWidget::is_task_running_for_test1703–1705 ↗
fn is_task_running_for_test(&self) -> bool

作用:测试专用:查看底部面板是否认为有任务正在运行。

数据流:它调用 bottom_pane.is_task_running → 输出布尔值。

调用关系:只在测试编译时可用,用来验证任务状态同步是否正确。

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

ChatWidget::toggle_vim_mode_and_notify1707–1715 ↗
fn toggle_vim_mode_and_notify(&mut self)

作用:切换 Vim 输入模式,并显示一条提示。Vim 模式是一种类似 Vim 编辑器的键盘操作方式。

数据流:它调用底部面板切换 Vim 开关 → 根据新状态生成启用或关闭消息 → 插入信息历史。

调用关系:用户切换输入模式时调用它,反馈由 add_info_message 完成。

调用图:调用 2 个内部函数(toggle_vim_enabled, add_info_message)。

ChatWidget::is_normal_backtrack_mode1720–1722 ↗
fn is_normal_backtrack_mode(&self) -> bool

作用:判断当前是否处在普通输入状态,能不能用 Esc-Esc 这类回退操作。

数据流:它询问 bottom_pane.is_normal_backtrack_mode → 输出布尔值。

调用关系:按键处理流程会用它决定 Esc 是否应该触发回退提示或行为。

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

ChatWidget::should_handle_vim_insert_escape1724–1727 ↗
fn should_handle_vim_insert_escape(&self, key_event: KeyEvent) -> bool

作用:判断 Vim 插入模式下的 Esc 键是否应该由输入框自己处理。

数据流:输入是按键事件 → 它转交给 bottom_pane.composer_should_handle_vim_insert_escape → 输出是否处理。

调用关系:按键分发时用它避免 ChatWidget 和输入框抢同一个 Esc。

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

ChatWidget::insert_str1729–1731 ↗
fn insert_str(&mut self, text: &str)

作用:把一段文字插入到底部输入框当前光标位置。

数据流:输入是文本切片 → 它调用 bottom_pane.insert_str → 输入框内容发生变化。

调用关系:粘贴、自动填充或外部插入文字时会用它。

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

ChatWidget::set_composer_text1734–1743 ↗
fn set_composer_text(
        &mut self,
        text: String,
        text_elements: Vec<TextElement>,
        local_image_paths: Vec<PathBuf>,
    )

作用:用给定内容替换输入框,并重置相关提示。

数据流:输入是文本、富文本元素和本地图片路径 → 它交给底部面板设置输入框内容 → 刷新计划模式提示。

调用关系:恢复草稿、编辑队列消息或外部填充输入时会使用它。

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

ChatWidget::set_remote_image_urls1745–1747 ↗
fn set_remote_image_urls(&mut self, remote_image_urls: Vec<String>)

作用:设置输入框里附带的远程图片链接。

数据流:输入是图片 URL 列表 → 它转交给 bottom_pane → 底部输入区保存这些远程图片。

调用关系:图片输入流程会调用它,让下一次提交带上图片。

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

ChatWidget::take_remote_image_urls1749–1751 ↗
fn take_remote_image_urls(&mut self) -> Vec<String>

作用:取走输入框里暂存的远程图片链接,并清空它们。

数据流:它调用 bottom_pane.take_remote_image_urls → 输出 URL 列表,同时底部面板不再持有这些链接。

调用关系:提交用户消息时会用它把图片从草稿转移到消息内容。

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

ChatWidget::remote_image_urls1754–1756 ↗
fn remote_image_urls(&self) -> Vec<String>

作用:测试专用:查看当前输入框暂存了哪些远程图片链接。

数据流:它调用 bottom_pane.remote_image_urls → 输出 URL 列表副本。

调用关系:只在测试中用于断言图片草稿状态。

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

ChatWidget::pending_thread_approvals1759–1761 ↗
fn pending_thread_approvals(&self) -> &[String]

作用:测试专用:查看当前哪些线程还在等审批。

数据流:它调用 bottom_pane.pending_thread_approvals → 输出内部列表引用。

调用关系:测试用它验证审批提示是否正确同步到底部面板。

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

ChatWidget::has_active_view1764–1766 ↗
fn has_active_view(&self) -> bool

作用:测试专用:判断底部面板现在是否打开了某个弹窗或子视图。

数据流:它调用 bottom_pane.has_active_view → 输出布尔值。

调用关系:测试用它确认反馈、设置、选择框等视图是否出现。

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

ChatWidget::show_esc_backtrack_hint1768–1770 ↗
fn show_esc_backtrack_hint(&mut self)

作用:让底部面板显示 Esc 回退提示,提醒用户可以返回上一步。

数据流:它调用 bottom_pane.show_esc_backtrack_hint → 底部提示状态被打开。

调用关系:按键引导流程会调用它。

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

ChatWidget::clear_esc_backtrack_hint1772–1774 ↗
fn clear_esc_backtrack_hint(&mut self)

作用:清掉底部的 Esc 回退提示。

数据流:它调用 bottom_pane.clear_esc_backtrack_hint → 底部提示状态被关闭。

调用关系:当用户继续输入、状态变化或不再适合提示时调用。

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

ChatWidget::refresh_skills_for_current_cwd1776–1781 ↗
fn refresh_skills_for_current_cwd(&mut self, force_reload: bool)

作用:按当前工作目录刷新可用技能列表。技能可以理解成模型可调用的一组预设能力或工具说明。

数据流:输入是是否强制重新加载 → 它用当前 cwd 构造 list_skills 命令 → 通过 submit_op 发给后台。

调用关系:技能刷新流程调用它;实际发送统一走 submit_op。

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

ChatWidget::submit_op1784–1806 ↗
fn submit_op(&mut self, op: T) -> bool

作用:把一个操作命令发给 Codex 后台。它是 UI 向核心系统下达指令的主要出口之一。

数据流:输入是可转成 AppCommand 的操作 → 它先做本地提交前准备;如果是审查操作则可能把任务状态设为运行中 → 按目标选择直接发通道或包装成 AppEvent 发送 → 输出是否发送成功。

调用关系:clean_background_terminals、refresh_skills_for_current_cwd 等调用它;它内部会调用 prepare_local_op_submission。

调用图:调用 5 个内部函数(send, is_task_running, set_task_running, prepare_local_op_submission, log_outbound_op);被 2 处调用(clean_background_terminals, refresh_skills_for_current_cwd);外部调用 4 个(into, is_review, CodexOp, error!)。

ChatWidget::append_message_history_entry1808–1815 ↗
fn append_message_history_entry(&self, text: String)

作用:把一段用户输入追加到当前线程的消息历史里,方便以后用历史记录找回。

数据流:输入是文本 → 它检查当前是否有 thread_id;没有就写警告日志并退出 → 有的话发送 AppendMessageHistoryEntry 事件。

调用关系:用户消息提交后可调用它保存输入历史。

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

ChatWidget::prepare_local_op_submission1817–1833 ↗
fn prepare_local_op_submission(&mut self, op: &AppCommand)

作用:在命令真正发给后台前,先更新本地 UI 状态,特别是处理中断命令。

数据流:输入是 AppCommand → 如果是中断且当前模型轮次在跑,它可能准备恢复草稿、清空流式队列、清掉活动流尾巴并刷新 → 其他命令基本不动。

调用关系:submit_op 每次发送前都会调用它,确保界面状态和后台命令同步。

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

ChatWidget::on_list_skills1835–1838 ↗
fn on_list_skills(&mut self, ev: SkillsListResponse)

作用:处理后台返回的技能列表。

数据流:输入是 SkillsListResponse → 它更新本地技能状态 → 然后刷新插件提及信息。

调用关系:技能列表事件到达时调用它;后续插件提及刷新由 refresh_plugin_mentions 完成。

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

ChatWidget::refresh_plugin_mentions1840–1847 ↗
fn refresh_plugin_mentions(&mut self)

作用:刷新输入框里可 @ 的插件提及候选。

数据流:它检查插件功能开关 → 没开就让底部面板清空插件提及;开了就发送 RefreshPluginMentions 事件 → 等外层加载结果。

调用关系:on_list_skills 会调用它,插件列表更新后也可用它重新拉取候选。

调用图:调用 2 个内部函数(send, set_plugin_mentions);被 1 处调用(on_list_skills)。

ChatWidget::on_plugin_mentions_loaded1849–1857 ↗
fn on_plugin_mentions_loaded(
        &mut self,
        plugins: Option<Vec<PluginCapabilitySummary>>,
    )

作用:插件提及候选加载回来后,把它们放进底部面板。

数据流:输入是可选插件列表 → 如果和底部面板已有列表一样,就不做事;否则更新 bottom_pane 的插件提及缓存。

调用关系:RefreshPluginMentions 的异步结果到达时调用它。

调用图:调用 2 个内部函数(plugins, set_plugin_mentions)。

ChatWidget::sync_plugin_mentions_config1859–1865 ↗
fn sync_plugin_mentions_config(&mut self, config: &Config)

作用:把和插件提及有关的配置同步到 ChatWidget 的配置副本里。

数据流:输入是新的 Config → 它复制功能开关、配置层、记忆设置和终端重排设置 → 同步 mentions v2 开关。

调用关系:配置变化后调用它,避免输入补全还按旧配置工作。

ChatWidget::token_usage1867–1872 ↗
fn token_usage(&self) -> TokenUsage

作用:返回当前累计 token 用量;如果还没有信息,就返回默认的空用量。

数据流:它读取 token_info → 有值就克隆 total_token_usage;没有就返回默认 TokenUsage。

调用关系:状态显示、统计或测试需要当前用量时会调用它。

ChatWidget::thread_id1874–1876 ↗
fn thread_id(&self) -> Option<ThreadId>

作用:返回当前会话线程 ID。

数据流:它读取 thread_id 字段 → 输出可选 ThreadId。

调用关系:add_mcp_output 会调用它把当前线程 ID 带到 FetchMcpInventory 请求里。

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

ChatWidget::thread_name1878–1880 ↗
fn thread_name(&self) -> Option<String>

作用:返回当前会话名的副本。

数据流:它读取 thread_name → 克隆并输出 Option<String>。

调用关系:外层 UI 或恢复提示需要显示会话名时会用它。

ChatWidget::rollout_path1887–1889 ↗
fn rollout_path(&self) -> Option<PathBuf>

作用:返回当前线程对应的 rollout 文件路径。rollout 可以理解成记录会话过程的持久化文件。

数据流:它读取 current_rollout_path → 克隆并输出可选路径。

调用关系:反馈、日志或会话持久化相关流程会查询它。

ChatWidget::active_cell_transcript_key1901–1924 ↗
fn active_cell_transcript_key(&self) -> Option<ActiveCellTranscriptKey>

作用:生成当前活动内容的转录缓存键。转录浮层用它判断“直播尾巴”有没有变,需不需要重新渲染。

数据流:它查看活动单元、活动钩子、token 活动和限流提示 → 如果都没有,输出 None;否则输出包含版本号、是否接续流和动画 tick 的 ActiveCellTranscriptKey。

调用关系:转录浮层 Ctrl+T 绘制时会用它决定缓存是否失效。

ChatWidget::active_cell_transcript_lines1966–1969 ↗
fn active_cell_transcript_lines(&self, width: u16) -> Option<Vec<Line<'static>>>

作用:测试专用:把活动转录行转成普通可见文本行。

数据流:输入是宽度 → 它先调用 active_cell_transcript_hyperlink_lines → 再去掉超链接包装,输出普通 Line 列表。

调用关系:测试用它验证活动单元在转录里的可见内容。

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

ChatWidget::config_ref1973–1975 ↗
fn config_ref(&self) -> &Config

作用:返回当前 ChatWidget 持有的配置引用。

数据流:它不修改任何状态 → 输出 &Config → 调用方可以读取当前运行时配置。

调用关系:外层需要查看 TUI 已应用的模型、审批策略等运行时覆盖时会用它。

ChatWidget::status_line_text1978–1980 ↗
fn status_line_text(&self) -> Option<String>

作用:测试专用:读取底部状态行当前显示的文字。

数据流:它调用 bottom_pane.status_line_text → 输出可选字符串。

调用关系:测试用它确认状态行渲染内容是否符合预期。

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

ChatWidget::clear_token_usage1982–1984 ↗
fn clear_token_usage(&mut self)

作用:清空当前保存的 token 用量信息。

数据流:它不接收输入 → 把 token_info 设为 None → 后续 token_usage 会返回默认空用量。

调用关系:会话重置或特定状态切换时可用它清掉旧统计。

has_websocket_timing_metrics1987–1994 ↗
fn has_websocket_timing_metrics(summary: RuntimeMetricsSummary) -> bool

作用:判断运行指标里是否包含 WebSocket/响应 API 的耗时数据。WebSocket 是一种保持连接、持续收发消息的网络通道。

数据流:输入是 RuntimeMetricsSummary → 它检查多个耗时字段是否大于 0 → 只要有一个存在就输出 true。

调用关系:性能或诊断显示逻辑会用它决定是否展示这些网络时序指标。

ChatWidget::drop1997–1999 ↗
fn drop(&mut self)

作用:ChatWidget 被销毁时做收尾,停止限流轮询器,避免后台定时任务继续跑。

数据流:对象生命周期结束 → drop 自动调用 stop_rate_limit_poller → 相关轮询资源被清理。

调用关系:这是 Rust 的析构钩子,不由普通代码手动调用;当聊天界面被替换或程序退出时自动发生。

extract_first_bold2021–2047 ↗
fn extract_first_bold(s: &str) -> Option<String>

作用:从一段 Markdown 文本里提取第一个加粗片段,也就是 里面的内容

数据流:输入是一段字符串 → 它逐字节寻找开头的 和结尾的 ,取中间并去空格 → 找到非空内容就输出字符串,否则输出 None。

调用关系:它是解析模型输出标题或摘要的小工具,遇到未闭合的加粗标记时会先不返回,等待后续文本。

tui/src/chatwidget/interrupts.rs源码 ↗
orchestrationrequest handling

聊天界面有时会被打断,比如程序要执行命令,需要用户批准;工具要改文件,需要用户同意;服务器想追问信息;或者后台任务开始、结束了。问题是,同一时间界面通常只能认真处理一个打断。如果多个打断一起冒出来,用户会看乱,程序也可能把回答对应错。这个文件用 InterruptManager 保存一个先进先出的队列,像银行排号一样,先来的先办。QueuedInterrupt 列出所有可能排队的事情。各种 push_xxx 函数负责把不同类型的事情塞进队尾。flush_all 会把队列从前往后取出来,交给 ChatWidget 立刻处理。还有一个重要动作是 remove_resolved_prompt:如果某个等待中的提问已经在别处被解决了,就把它从队列里删掉,避免用户之后又看到一个已经过期的问题。测试重点确认:只删匹配的请求,不误删别的,也不删任务开始/结束这类生命周期消息。

函数细节18
InterruptManager::new36–40 ↗
fn new() -> Self

作用:创建一个新的打断队列管理器。刚创建时队列是空的,后面聊天界面收到各种审批或提问时,就可以往里面放。

数据流:进去没有业务数据 → 它新建一个空的 VecDeque,也就是可以从前面取、从后面放的队列 → 出来一个 InterruptManager,里面还没有任何待处理打断。

调用关系:它是这个小系统的起点。正常界面初始化时会用它准备好排队能力;测试里也用它先做一个干净的管理器,再往里放不同的打断来检查行为。

调用图:被 4 处调用(new_with_op_target, remove_resolved_prompt_keeps_lifecycle_events, remove_resolved_prompt_matches_exec_approval_id, remove_resolved_prompt_removes_matching_user_input_only);外部调用 1 个(new)。

InterruptManager::is_empty43–45 ↗
fn is_empty(&self) -> bool

作用:检查当前有没有排队等待的打断。调用者可以用它判断是否还有事情没处理。

数据流:进去是这个管理器当前保存的队列 → 它直接询问底层队列是否为空 → 出来一个真假值:true 表示没有待处理打断,false 表示还有。

调用关系:它不改变任何东西,只是一个状态查询。别的流程可以在决定是否继续显示下一个弹窗、是否结束某段处理时使用它。

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

InterruptManager::push_exec_approval47–49 ↗
fn push_exec_approval(&mut self, ev: ExecApprovalRequestEvent)

作用:把“执行命令需要用户批准”这件事放进等待队列。比如程序想运行一条系统命令,但必须先问用户可不可以。

数据流:进去一个 ExecApprovalRequestEvent,里面有命令、工作目录、审批编号等信息 → 它把这个事件包成 QueuedInterrupt::ExecApproval → 放到队列末尾,等待之后处理。

调用关系:当聊天界面当前正忙,不能马上展示执行审批时,会调用它先存起来。之后 flush_all 会把它取出,并交给 ChatWidget 的 handle_exec_approval_now 立刻显示和处理。

调用图:外部调用 2 个(push_back, ExecApproval)。

InterruptManager::push_apply_patch_approval51–54 ↗
fn push_apply_patch_approval(&mut self, ev: ApplyPatchApprovalRequestEvent)

作用:把“修改文件需要用户批准”这件事放进队列。它用于避免文件改动审批弹窗和其他弹窗同时抢屏幕。

数据流:进去一个 ApplyPatchApprovalRequestEvent,表示某次文件变更审批 → 它包装成 QueuedInterrupt::ApplyPatchApproval → 加到队列末尾。

调用关系:上游收到文件改动审批但暂时不能处理时会用它排队。之后 flush_all 取出这类事件,并交给 ChatWidget 的 handle_apply_patch_approval_now 去真正展示审批。

调用图:外部调用 2 个(push_back, ApplyPatchApproval)。

InterruptManager::push_elicitation56–63 ↗
fn push_elicitation(
        &mut self,
        request_id: AppServerRequestId,
        params: McpServerElicitationRequestParams,
    )

作用:把服务器发来的“请补充信息”请求放进队列。elicitation 可以理解成服务端向用户追问一段必要信息。

数据流:进去一个请求编号 request_id 和一组请求参数 params,参数里包含服务器名等信息 → 它们一起被包成 QueuedInterrupt::Elicitation → 放到队列末尾等待处理。

调用关系:当应用服务器或 MCP 服务器需要用户补充信息但界面正被别的打断占用时,会先调用它。flush_all 后续会把请求交给 ChatWidget 的 handle_elicitation_request_now。

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

InterruptManager::push_request_permissions65–68 ↗
fn push_request_permissions(&mut self, ev: RequestPermissionsEvent)

作用:把“请求更多权限”的事件放进队列。比如工具想做一件当前权限不允许的事,就需要问用户是否放行。

数据流:进去一个 RequestPermissionsEvent,描述这次权限请求 → 它被包装成 QueuedInterrupt::RequestPermissions → 存到队列尾部。

调用关系:权限请求不能马上弹出时会通过这里排队。之后 flush_all 会把它交给 ChatWidget 的 handle_request_permissions_now,让界面正式询问用户。

调用图:外部调用 2 个(push_back, RequestPermissions)。

InterruptManager::push_user_input70–72 ↗
fn push_user_input(&mut self, ev: ToolRequestUserInputParams)

作用:把工具发来的“需要用户输入”的请求放进队列。比如工具需要用户回答几个问题,才能继续执行。

数据流:进去一个 ToolRequestUserInputParams,里面有线程、条目、回合和问题列表 → 它被包成 QueuedInterrupt::RequestUserInput → 放到队列末尾。

调用关系:工具请求用户输入但界面暂时不能显示时,会调用它。flush_all 之后会交给 ChatWidget 的 handle_request_user_input_now,让用户真正看到问题。

调用图:外部调用 2 个(push_back, RequestUserInput)。

InterruptManager::push_item_started74–76 ↗
fn push_item_started(&mut self, item: ThreadItem)

作用:把“某个线程项目开始了”的消息放进队列。它不是要用户回答的问题,而是为了让界面稍后补上任务开始的状态变化。

数据流:进去一个 ThreadItem,表示某个任务或命令等项目 → 它被包成 QueuedInterrupt::ItemStarted → 追加到队列末尾。

调用关系:当界面正在显示别的打断,不能马上刷新项目开始状态时,会先排队。flush_all 后会交给 ChatWidget 的 handle_queued_item_started_now 更新界面。

调用图:外部调用 2 个(push_back, ItemStarted)。

InterruptManager::push_item_completed78–80 ↗
fn push_item_completed(&mut self, item: ThreadItem)

作用:把“某个线程项目完成了”的消息放进队列。这样即使界面当时被弹窗占用,任务完成状态也不会丢。

数据流:进去一个 ThreadItem,表示已经完成或有完成状态的项目 → 它被包成 QueuedInterrupt::ItemCompleted → 追加到队列末尾。

调用关系:后台任务结束但界面暂时不能处理时会用它。flush_all 后会把它交给 ChatWidget 的 handle_queued_item_completed_now,用来补更新完成状态。

调用图:外部调用 2 个(push_back, ItemCompleted)。

InterruptManager::remove_resolved_prompt82–87 ↗
fn remove_resolved_prompt(&mut self, request: &ResolvedAppServerRequest) -> bool

作用:从队列里删掉已经被解决的提问或审批,避免用户之后又看到一个过期弹窗。它会返回这次到底有没有删到东西。

数据流:进去一个 ResolvedAppServerRequest,表示某个请求已经有结果了 → 它记下原队列长度,然后保留所有“不匹配这个已解决请求”的队列项 → 出来一个真假值:true 表示至少删掉了一个匹配项,同时队列也被更新;false 表示队列没变。

调用关系:dismiss_app_server_request 会调用它,通常发生在应用服务器请求被取消、关闭或已经处理完时。它内部依靠 QueuedInterrupt::matches_resolved_prompt 判断每个排队项是不是对应这个已解决请求。

调用图:被 1 处调用(dismiss_app_server_request);外部调用 2 个(len, retain)。

InterruptManager::flush_all89–105 ↗
fn flush_all(&mut self, chat: &mut ChatWidget)

作用:把队列里攒着的所有打断按顺序拿出来,立刻交给聊天界面处理。它相当于告诉排队的人:“现在可以一个个进来了。”

数据流:进去一个可修改的 InterruptManager 和一个可修改的 ChatWidget → 它不断从队列最前面取出一项,根据类型选择对应的 ChatWidget 处理函数 → 出来时队列被清空,聊天界面已经收到并处理了这些事件。

调用关系:它是排队到实际显示之间的出口。前面的 push_xxx 函数只负责存,flush_all 负责把存下来的事件分发给 handle_exec_approval_now、handle_apply_patch_approval_now、handle_elicitation_request_now 等真正处理界面的函数。

调用图:外部调用 8 个(pop_front, handle_apply_patch_approval_now, handle_elicitation_request_now, handle_exec_approval_now, handle_queued_item_completed_now, handle_queued_item_started_now, handle_request_permissions_now, handle_request_user_input_now)。

QueuedInterrupt::matches_resolved_prompt109–135 ↗
fn matches_resolved_prompt(&self, request: &ResolvedAppServerRequest) -> bool

作用:判断某个排队中的打断,是否正好对应一个已经解决的请求。这个判断用来安全删除过期弹窗。

数据流:进去一个队列项 self 和一个已解决请求 request → 它按队列项类型逐一比较关键编号,比如审批 id、文件改动 call_id、服务器名和请求 id、用户输入 item_id → 出来 true 或 false;任务开始和任务完成消息永远返回 false,因为它们不是可解决的提示框。

调用关系:它主要服务于 InterruptManager::remove_resolved_prompt。remove_resolved_prompt 扫描队列时,会把每一项交给它判断,只有真正匹配的提示类事件才会被移除。

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

tests::user_input149–157 ↗
fn user_input(call_id: &str, turn_id: &str) -> ToolRequestUserInputParams

作用:测试用的小帮手,用来快速造一个“请求用户输入”的假事件。这样测试不用每次手写一大堆字段。

数据流:进去一个 call_id 和一个 turn_id → 它填好线程编号、条目编号、回合编号,并给问题列表和自动解决时间放默认空值 → 出来一个 ToolRequestUserInputParams 测试对象。

调用关系:它只在测试里使用,主要被 tests::remove_resolved_prompt_removes_matching_user_input_only 调用,用来准备两个不同编号的用户输入请求。

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

tests::exec_approval159–173 ↗
fn exec_approval(call_id: &str, approval_id: Option<&str>) -> ExecApprovalRequestEvent

作用:测试用的小帮手,用来造一个“执行命令审批”的假事件。它让测试能专注检查匹配规则,而不是忙着填结构体字段。

数据流:进去一个 call_id 和一个可选的 approval_id → 它填入固定命令 true、当前目录、回合编号以及一批空的可选信息 → 出来一个 ExecApprovalRequestEvent 测试对象。

调用关系:它被 tests::remove_resolved_prompt_matches_exec_approval_id 使用,用来确认执行审批匹配时看的是有效审批编号,而不是随便用调用编号误删。

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

tests::command_execution175–188 ↗
fn command_execution(call_id: &str) -> ThreadItem

作用:测试用的小帮手,用来造一个正在执行中的命令项目。这里主要拿它模拟“项目开始”这类生命周期消息。

数据流:进去一个 call_id → 它创建一个 ThreadItem::CommandExecution,填入命令 true、当前目录、来源、执行中状态和空的输出信息 → 出来一个 ThreadItem 测试对象。

调用关系:它被 tests::remove_resolved_prompt_keeps_lifecycle_events 调用,用来证明 remove_resolved_prompt 不会把任务开始这类状态消息当成审批提示删掉。

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

tests::remove_resolved_prompt_removes_matching_user_input_only191–207 ↗
fn remove_resolved_prompt_removes_matching_user_input_only()

作用:这个测试确认:删除已解决的用户输入请求时,只会删除编号匹配的那一个,不会把其他等待中的用户输入一起删掉。

数据流:进去没有外部输入 → 它新建管理器,放入 call-a 和 call-b 两个用户输入请求,然后声明 call-b 已解决 → 结果应当返回 true,队列只剩 call-a;如果剩下的不是预期对象,测试会失败。

调用关系:它验证 InterruptManager::remove_resolved_prompt 和 QueuedInterrupt::matches_resolved_prompt 对用户输入请求的配合是否正确。tests::user_input 负责提供测试数据。

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

tests::remove_resolved_prompt_matches_exec_approval_id210–227 ↗
fn remove_resolved_prompt_matches_exec_approval_id()

作用:这个测试确认:执行命令审批如果有单独的 approval_id,就应该用这个审批编号来匹配,而不是用 call_id。

数据流:进去没有外部输入 → 它创建一个带 call_id 为 call、approval_id 为 approval 的执行审批 → 先用 call 去删除,应当删不到;再用 approval 去删除,应当删到并清空队列。

调用关系:它专门保护执行审批的匹配规则。它调用 tests::exec_approval 造数据,再通过 InterruptManager::remove_resolved_prompt 检查结果。

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

tests::remove_resolved_prompt_keeps_lifecycle_events230–245 ↗
fn remove_resolved_prompt_keeps_lifecycle_events()

作用:这个测试确认:任务开始、任务完成这类生命周期消息不会因为某个审批被解决而被误删。它们不是弹窗问题,而是界面状态更新。

数据流:进去没有外部输入 → 它新建管理器,放入一个 ItemStarted 消息,再尝试用同样 id 的执行审批解决请求去删除 → 结果应当删不到,队列里仍然保留 ItemStarted。

调用关系:它验证 QueuedInterrupt::matches_resolved_prompt 对 ItemStarted 和 ItemCompleted 返回 false 的设计。tests::command_execution 负责造出用于排队的线程项目。

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

tui/src/chatwidget/notifications.rs源码 ↗
domain_logicmain loop / notification handling

这个文件像一个“通知前台”。聊天界面里会发生很多事:AI 回答完了、需要用户批准运行命令、想修改文件、外部服务要用户确认、计划模式需要选择。它不会马上把每件事都弹到桌面上,而是先检查用户设置:通知是不是开启,或者只允许某几类通知。通过后,它还会看优先级:普通的“AI 回答完了”优先级低,审批请求和计划提示优先级高。如果已经有一个更重要的通知在排队,新的低优先级通知就会被丢掉。真正显示时,它会把通知内容变成短短一句话,比如把长命令截短,或者把 AI 的长回复压成预览。这样用户看到的是有用、简短、不过度打扰的桌面提示。

函数细节8
ChatWidget::notify6–17 ↗
fn notify(&mut self, notification: Notification)

作用:这个函数把一个新通知放进聊天界面的“待发送通知”位置。它会先看用户是否允许这类通知,再看它是不是比当前排队的通知更重要,避免低价值提示盖掉重要提醒。

数据流:进去的是一个 Notification 通知对象,以及 ChatWidget 里保存的通知设置和当前待发通知。它先用 allowed_for 检查设置;如果不允许,就什么也不改。然后如果已有待发通知,并且已有通知优先级更高,就丢弃新通知。否则把新通知保存到 pending_notification,并要求界面重画。出来没有返回值,但聊天界面的待发通知状态可能被更新。

调用关系:它是聊天界面内部收到“需要提醒用户”事件时的入口。它把判断工作交给 Notification::allowed_for 和 Notification::priority,自己负责最后决定要不要把通知排队,并触发后续重画,让稍后的流程有机会真正发出通知。

调用图:调用 2 个内部函数(allowed_for, priority)。

ChatWidget::maybe_post_pending_notification19–23 ↗
fn maybe_post_pending_notification(&mut self, tui: &mut crate::tui::Tui)

作用:这个函数负责把已经排队的通知真正发到桌面通知系统。它像“下班前清空前台留言”:有通知就发,没有就不做事。

数据流:进去的是可修改的 ChatWidget 和 Tui 对象。它从 pending_notification 里取出通知;如果取到了,就调用通知的 display 生成一段给人看的文字,再交给 tui.notify 发出去。出来没有返回值,但 pending_notification 会被清空,桌面系统可能收到一条通知。

调用关系:ChatWidget::notify 只是先把通知存起来,这个函数则在合适的界面刷新或事件循环时机把它送出去。它最后把发送动作交给外部的 tui.notify,自己不直接接触操作系统通知细节。

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

Notification::display36–66 ↗
fn display(&self) -> String

作用:这个函数把不同种类的通知变成用户能看懂的一句话。它的重点是短、清楚,比如“需要批准某个命令”或“Codex 想编辑某个文件”。

数据流:进去的是一个具体的 Notification。它根据通知类型取出里面的数据:AI 回复、命令、文件路径、服务名或标题;然后整理成短文本,必要时截断太长的内容。出来是一段 String 字符串,也就是最终要显示在桌面通知里的文字。

调用关系:它通常由 ChatWidget::maybe_post_pending_notification 在真正发送通知前调用。遇到 AI 回合结束的通知时,它会把长回复交给 Notification::agent_turn_preview 做预览;其他通知则直接拼出简短说明。

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

Notification::type_name68–76 ↗
fn type_name(&self) -> &str

作用:这个函数给每种通知取一个内部用的类别名字。这个名字不是主要给用户看的,而是用来和配置里的允许列表对照。

数据流:进去的是一个 Notification。它看通知属于哪一类,然后返回固定的短名称,比如 agent-turn-complete、approval-requested 或 plan-mode-prompt。出来的是一个字符串切片,表示这类通知的内部名称。

调用关系:它服务于 Notification::allowed_for。用户配置如果选择“只允许某些通知”,allowed_for 就靠这里返回的类别名来判断当前通知能不能弹。

Notification::priority78–86 ↗
fn priority(&self) -> u8

作用:这个函数给通知分重要程度。AI 回答完成是低优先级,审批请求和计划提示是高优先级,这样重要事情不会被普通提示挤掉。

数据流:进去的是一个 Notification。它根据通知类型返回一个数字:0 表示较低,1 表示较高。出来的是这个优先级数字,不会改动任何状态。

调用关系:它被 ChatWidget::notify 调用,用来比较新通知和已经排队通知的重要性。如果旧通知优先级更高,新通知就不会覆盖它。

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

Notification::allowed_for88–93 ↗
fn allowed_for(&self, settings: &Notifications) -> bool

作用:这个函数判断当前通知是否符合用户的通知设置。用户可能完全开关通知,也可能只允许某些类别通知弹出。

数据流:进去的是一个 Notification 和 Notifications 设置。如果设置是简单开关,它直接返回开还是关;如果设置是自定义允许列表,它会拿当前通知的 type_name 去列表里找。出来是 true 或 false,表示能不能继续排队发送。

调用关系:它被 ChatWidget::notify 在最开始调用,相当于通知发出前的门卫。只有通过这里的检查,后面才会继续比较优先级并保存到待发通知。

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

Notification::agent_turn_preview95–109 ↗
fn agent_turn_preview(response: &str) -> Option<String>

作用:这个函数把 AI 的长回复压成适合桌面通知显示的短预览。它会清理多余空白,并限制长度,避免通知框里塞进一大段文字。

数据流:进去的是 AI 回复原文 response。它按空白拆开,再用单个空格重新拼起来,把换行和多余空格压平;如果结果为空,就返回 None;如果有内容,就截到固定长度后返回 Some 字符串。它不改动外部状态。

调用关系:Notification::display 在生成“AI 回答完成”通知时会调用它。调用图里也显示 on_task_complete 会直接用它,说明任务完成时也可能需要先做这份回复预览。

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

Notification::user_input_request_summary111–125 ↗
fn user_input_request_summary(
        questions: &[codex_app_server_protocol::ToolRequestUserInputQuestion],
    ) -> Option<String>

作用:这个函数从一组“需要用户输入的问题”里提取一句适合通知显示的摘要。它优先用问题标题;没有标题时,才用问题正文。

数据流:进去的是问题列表 questions。它只看第一个问题;如果没有问题,就返回 None。然后取 header 标题并去掉前后空白;标题为空时改用 question 正文。最后如果摘要不空,就截短到 30 个可见字符左右并返回 Some 字符串,否则返回 None。

调用关系:它被 handle_request_user_input_now 调用,用在系统需要立刻向用户要输入的时候。它不负责发送通知,只负责把复杂的问题内容压成一句能放进通知或提示里的短说明。

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

tui/src/chatwidget/rendering.rs源码 ↗
domain_logicmain loop / redraw

聊天窗口不是一次性画一整块,而是像拼积木:聊天正文、正在活动的单元格、临时提示、限速提示、底部输入框,各占一块地方。这个文件就是拼装规则。它先把 ChatWidget 转成一个统一的 Renderable(可绘制对象,也就是“知道怎么把自己画到终端上的东西”),再用 FlexRenderable(弹性布局,类似网页里的按比例分配空间)把这些块上下排好。为了避免右侧的宠物或装饰内容挡住文字,它会给正文和输入区预留右边空白。TranscriptAreaRenderable 专门画聊天记录,会在内容太高时自动滚到最后,让用户看到最新内容。BottomPaneComposerReserveRenderable 则是底部输入区的转接头,把“右侧要留多少空位”这件事传进去。最后 ChatWidget 自己也实现 Renderable,让外层界面只要叫它“画一下”“告诉我多高”“光标在哪”,它就能把请求交给拼好的内部结构。

函数细节12
ChatWidget::as_renderable6–60 ↗
fn as_renderable(&self) -> RenderableItem<'_>

作用:把整个聊天窗口临时组装成一个可绘制的界面树。别人不用知道里面有多少块,只要拿到这个结果,就能统一地画、算高度、找光标。

数据流:进去的是 ChatWidget 当前状态:当前聊天单元、可能存在的 hook 单元、临时 token 活动提示、限速提示、底部输入区,以及右侧需要预留的宽度。它把这些内容分别包成可绘制对象,再按上下顺序放进弹性布局里;底部输入区还额外加了上方一点空隙。出来的是一个 RenderableItem,代表“已经拼好的聊天界面”。

调用关系:这是本文件的核心拼装点。ChatWidget::render、ChatWidget::desired_height、ChatWidget::cursor_pos、ChatWidget::cursor_style 都会先调用它,再把具体工作交给组装出的界面树。它内部会创建 TranscriptAreaRenderable 和 BottomPaneComposerReserveRenderable,并用布局对象把它们串起来。

调用图:调用 2 个内部函数(tlbr, new);被 4 处调用(cursor_pos, cursor_style, desired_height, render);外部调用 2 个(new, Owned)。

BottomPaneComposerReserveRenderable::render69–72 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把底部输入区画出来,同时告诉它右边要空出一段位置,避免输入内容和右侧保留区域撞在一起。

数据流:进去的是可用的终端区域 area、要写入的屏幕缓冲区 buf,以及结构里保存的 bottom_pane 和 right_reserve。它不自己画细节,而是把这些信息交给底部输入区的 render_with_composer_right_reserve。结果是底部输入区被画进缓冲区,并按要求留出右侧空白。

调用关系:它是底部输入区的“适配器”。as_renderable 把它放进整体布局里;当布局轮到底部区域需要绘制时,会走到这个函数,然后它把真正的绘制交给 BottomPane。

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

BottomPaneComposerReserveRenderable::desired_height74–77 ↗
fn desired_height(&self, width: u16) -> u16

作用:询问底部输入区在给定宽度下需要多高,并把右侧预留空间也算进去。

数据流:进去的是外层给的宽度 width,以及保存的 right_reserve。它把宽度和预留信息传给 desired_height_with_composer_right_reserve。出来的是一个高度数字,告诉布局系统底部输入区至少需要几行。

调用关系:当外层要计算整个聊天窗口高度时,as_renderable 组装出的布局会询问每个子块的高度;轮到底部输入区时,就通过这个函数问 BottomPane。

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

BottomPaneComposerReserveRenderable::cursor_pos79–82 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>

作用:告诉外层:底部输入区里的文字光标应该显示在终端的哪个位置,并且计算时考虑右侧预留空间。

数据流:进去的是底部输入区所在的区域 area,以及保存的 right_reserve。它把这些传给 cursor_pos_with_composer_right_reserve。出来的是一个可选坐标:有光标就返回位置,没有就返回空。

调用关系:当外层界面需要定位输入光标时,会一路问到当前可绘制对象。这个包装器不自己算坐标,而是把问题交给 BottomPane,因为只有底部输入区最清楚输入光标在哪里。

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

BottomPaneComposerReserveRenderable::cursor_style84–87 ↗
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle

作用:告诉终端光标应该用什么样式,比如块状、竖线或其他形态,同时考虑底部输入区的状态。

数据流:进去的是底部输入区区域 area 和右侧预留宽度。它调用 cursor_style_with_composer_right_reserve,让 BottomPane 根据当前输入状态决定光标样式。出来的是一个终端光标样式设置。

调用关系:ChatWidget::cursor_style 最终会通过组装出的界面树问到这里。这里仍然只是转交问题,因为光标样式由底部输入区自己的逻辑决定。

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

TranscriptAreaRenderable::render97–111 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:把一段聊天记录或提示内容画到它自己的区域里。内容太多放不下时,它会自动显示靠后的部分,让最新文字更容易被看见。

数据流:进去的是外层分配的区域 area、屏幕缓冲区 buf,以及 child 这个聊天内容单元。它先用 child_area 算出真正可写的内部区域,再向 child 要适合当前宽度的显示行;接着把这些行变成段落,计算是否超出高度,如果超出就向下滚动到尾部;最后先清空该区域,再把段落画进去。出来的结果是缓冲区中对应区域被更新。

调用关系:as_renderable 会把当前聊天单元、hook 单元和各种临时提示包装成 TranscriptAreaRenderable。真正绘制时,这个函数负责把单个历史单元画好;它会调用 TranscriptAreaRenderable::child_area 来扣掉顶部空隙和右侧预留。

调用图:调用 1 个内部函数(child_area);外部调用 5 个(new, from, display_lines, try_from, from)。

TranscriptAreaRenderable::desired_height113–116 ↗
fn desired_height(&self, width: u16) -> u16

作用:计算某个聊天内容块在指定宽度下理想上需要多少行,方便外层布局分配空间。

数据流:进去的是外层给的宽度 width,以及结构里保存的右侧预留 right 和顶部空隙 top。它先从宽度里扣掉右侧预留,保证至少还有 1 列可用;然后问 child 自己需要多少高度,最后再加上顶部空隙。出来的是总高度。

调用关系:整体布局在决定各块高度时会用到它。它把具体内容需要几行这件事交给 HistoryCell::desired_height,因为每种聊天单元最清楚自己的文字会占多少行。

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

TranscriptAreaRenderable::child_area120–129 ↗
fn child_area(&self, area: Rect) -> Rect

作用:从外层给的大区域里切出真正给聊天内容使用的小区域。它会扣掉上方留白和右侧保留宽度。

数据流:进去的是外层区域 area,以及结构里保存的 top 和 right。它把 y 坐标往下移 top 行,高度减掉 top,宽度减掉 right,并保证宽度最少为 1。出来的是一个新的 Rect,表示内容实际绘制的位置和大小。

调用关系:TranscriptAreaRenderable::render 会先调用它,拿到准确的绘制范围后才去排版和画文字。它是一个小工具,但能避免正文内容画到不该画的地方。

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

ChatWidget::render133–136 ↗
fn render(&self, area: Rect, buf: &mut Buffer)

作用:这是聊天窗口真正被要求“画到屏幕上”时走的入口。它把窗口组装成可绘制结构,然后执行绘制,并记住这次使用的宽度。

数据流:进去的是要绘制的区域 area 和屏幕缓冲区 buf。它调用 as_renderable 得到完整界面树,再让界面树把内容画进缓冲区;画完后,把 area 的宽度保存到 last_rendered_width。出来没有单独返回值,但缓冲区被更新,ChatWidget 也记住了最近一次渲染宽度。

调用关系:外层终端界面刷新时会调用这个函数。它本身不画每一行文字,而是先调用 ChatWidget::as_renderable,把活儿分给内部拼好的各个可绘制部件。

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

ChatWidget::desired_height138–140 ↗
fn desired_height(&self, width: u16) -> u16

作用:告诉外层布局:聊天窗口在某个宽度下想要多高。这样外层才能合理安排屏幕空间。

数据流:进去的是可用宽度 width。它先调用 as_renderable 组装当前界面树,再询问这个界面树的理想高度。出来的是高度数字。

调用关系:外层布局计算尺寸时会用到它。它和 render 使用同一套 as_renderable 拼装逻辑,所以“怎么算高度”和“实际怎么画”保持一致,不容易出现算出来和画出来不匹配的情况。

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

ChatWidget::cursor_pos142–144 ↗
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)>

作用:告诉外层终端光标应该放在哪里,通常用于把光标放到输入框里正确的位置。

数据流:进去的是聊天窗口所在区域 area。它调用 as_renderable 组装界面树,再询问这个界面树当前光标坐标。出来的是一个可选坐标:如果当前有可显示的光标就返回位置,否则返回空。

调用关系:外层界面在绘制后或准备显示光标时会调用它。它不自己判断光标在哪,而是通过 as_renderable 把问题交给当前真正包含光标的子部件,例如底部输入区。

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

ChatWidget::cursor_style146–148 ↗
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle

作用:告诉外层终端光标该显示成什么样子,让输入体验和当前状态匹配。

数据流:进去的是聊天窗口区域 area。它先通过 as_renderable 得到当前界面树,再询问界面树的光标样式。出来的是一个 crossterm 光标样式;crossterm 是和终端打交道的库,这里用它来控制光标外观。

调用关系:外层终端渲染流程需要设置光标样式时会调用它。它把决定权交给 as_renderable 组装出的子部件,尤其是底部输入区,因为那里最知道当前应该用哪种光标。

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

交互和输入流程

涵盖从键盘/composer 操作到队列输入管理、恢复和最终提交的用户交互处理。

tui/src/chatwidget/input_queue.rs源码 ↗
domain_logicmain loop / user input handling

聊天界面不是用户一敲回车就一定马上发送。比如上一轮对话还在进行,新的输入就要先排队;有些“引导”消息本来想改变当前对话方向,但时机不对,会被放进重试队列;还有一些消息已经交给核心系统,但还没正式写进历史记录。这个文件把这些状态集中放在 InputQueueState 里。它用队列(先进先出,像排队买票)保存不同种类的输入,并额外保存“历史记录该怎么显示”,因为有些斜杠命令实际发送的内容和界面上显示的文字不一样。它还提供清空、判断是否有后续消息、生成预览这几个小动作。PendingInputPreview 则是给界面看的摘要,把普通排队消息、等待中的引导、被拒后要重试的引导分开列出来,方便用户看清楚当前还有什么没处理。

函数细节5
InputQueueState::has_queued_follow_up_messages48–50 ↗
fn has_queued_follow_up_messages(&self) -> bool

作用:这个函数用来快速判断:当前有没有还没处理完的后续用户输入。它只关心两类最需要继续发送的内容:普通排队消息,以及之前被拒绝、需要优先重试的引导消息。

数据流:进去的是当前 InputQueueState 里的几个队列状态。它检查“被拒引导队列”和“普通用户消息队列”是不是为空。只要其中一个不为空,就出来 true,表示还有东西等着处理;两个都空,就出来 false。它不改动任何数据。

调用关系:聊天界面在决定下一步要不要继续自动发送、或者是否还有用户输入待处理时,会用这个函数做一个简单判断。它内部只把判断工作交给队列自带的 is_empty 方法。

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

InputQueueState::clear52–60 ↗
fn clear(&mut self)

作用:这个函数把所有等待中的输入状态一次性清干净。它适合在会话重置、取消当前流程、或需要把输入排队区恢复到初始状态时使用。

数据流:进去的是一个可修改的 InputQueueState。它把普通消息队列、普通消息历史记录、被拒引导队列、被拒引导历史记录、等待确认的引导全部清空,并把几个表示“正在等开始”“中断后要重新提交”等开关关掉。出来时没有返回值,但这个状态对象已经变成干净的空状态。

调用关系:它是输入队列的“清场按钮”。外层的 ChatWidget 或相关流程在需要放弃当前积压输入时会调用它;它内部依次调用各个队列自己的 clear 方法,把具体清空动作交给底层容器完成。

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

InputQueueState::preview62–95 ↗
fn preview(&self) -> PendingInputPreview

作用:这个函数把当前所有没处理完的输入整理成一份给界面展示的预览。它会把三类内容分开放:普通排队消息、已经提交但还没进历史的引导、被拒后等待重试的引导。

数据流:进去的是当前 InputQueueState。它读取三个队列,并尽量配上对应的历史显示记录;如果有专门的显示记录,就按记录生成文字,否则按用户消息本身生成文字。最后出来一个 PendingInputPreview,里面有三个字符串列表。它只读取数据,不改变队列。

调用关系:这是状态和界面之间的翻译器。ChatWidget 想显示“还有哪些输入在等”时会用它。它遍历各个队列,并把每条消息交给 user_message_preview_text 变成适合人看的文字。

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

tests::preview_keeps_queue_categories_separate105–130 ↗
fn preview_keeps_queue_categories_separate()

作用:这个测试确认 preview 不会把不同种类的等待输入混在一起。普通排队消息、等待中的引导、被拒的引导,应该各回各的列表。

数据流:它先新建一个空的 InputQueueState,再分别塞入一条普通排队消息、一条被拒引导、一条等待中的引导。然后调用 preview,检查输出的三个列表是否分别只包含对应那一条文字。测试本身不产生业务结果,只验证代码行为是否符合预期。

调用关系:这是给 InputQueueState::preview 的保护网。以后有人改预览逻辑时,如果不小心把类别合并或放错位置,这个测试会失败提醒。它使用 from 创建测试消息,用 default 创建初始状态,用 assert_eq! 比较实际结果和期望结果。

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

tests::clear_resets_all_input_queues133–153 ↗
fn clear_resets_all_input_queues()

作用:这个测试确认 clear 真的把所有相关输入状态都重置了,而不是只清掉一部分。它特别检查队列和几个布尔开关都回到空或关闭状态。

数据流:它先创建一个 InputQueueState,并故意放入排队消息、被拒引导,还把几个状态开关打开。接着调用 clear。最后逐项检查:各个队列为空,user_turn_pending_start 为 false,submit_pending_steers_after_interrupt 也为 false。测试只验证状态变化,不返回业务数据。

调用关系:这是给 InputQueueState::clear 的安全检查。因为 clear 是“清场按钮”,漏清任何一项都可能让后续聊天流程误以为还有旧输入要处理。它使用 from 准备消息,用 default 准备初始状态,用 assert! 检查每个条件。

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

tui/src/chatwidget/input_restore.rs源码 ↗
orchestrationmain loop / request handling / interruption restore / thread switch

聊天界面里,用户输入的不只是几行文字,还可能有图片、粘贴内容、提及对象,以及已经排队但还没发送的后续消息。这个文件就像输入框的“保险箱”和“回收站”:该暂存的暂存,该恢复的恢复,该合并的合并。比如模型正在回答时用户又输入了新指令,这些内容可能先排队;如果当前回答被打断,文件会决定是马上发送这些指令,还是把它们重新放回输入框。它还会处理一个容易出错的细节:多个消息合并时,图片或粘贴占位符不能撞名,否则用户看到的内容和实际附件会对不上。这里的函数大多围绕 ChatWidget 工作,也就是聊天界面的主控件,负责把底部输入框、输入队列、历史记录和运行状态协调好。

函数细节18
ChatWidget::record_cancel_edit_candidate9–13 ↗
fn record_cancel_edit_candidate(&mut self, prompt: UserMessage)

作用:记下一个“如果稍后取消,可以恢复回输入框”的用户提示词。它用于支持用户中断后回到刚才正在编辑的内容。

数据流:进去的是一个 UserMessage,也就是用户的一条完整输入;函数把它存进 cancel_edit.prompt,并标记为“有资格恢复”,但还没有真正进入待恢复状态;出来没有返回值,只改变 ChatWidget 内部的取消编辑记录。

调用关系:它通常发生在系统判断某条输入可能需要被取消恢复的时候。后面 ChatWidget::arm_cancel_edit 会根据当前输入框和队列是否干净,决定这条记录能不能真正启用。

ChatWidget::record_visible_turn_activity15–18 ↗
fn record_visible_turn_activity(&mut self)

作用:告诉界面:当前对话已经出现了可见活动,所以之前那种“可以取消并恢复输入”的机会不再安全。简单说,就是一旦事情已经明显发生了,就不要再假装还能无痕撤回。

数据流:它不接收额外输入;读取并修改 cancel_edit 状态,把 eligible 和 armed 都设为 false;没有返回值,结果是取消恢复机制被关掉。

调用关系:它和 ChatWidget::record_cancel_edit_candidate、ChatWidget::arm_cancel_edit 配合,避免在模型已经有可见动作后还把旧输入恢复回来,造成用户误解。

ChatWidget::arm_cancel_edit20–27 ↗
fn arm_cancel_edit(&mut self)

作用:真正“上膛”取消恢复功能。只有输入框为空、没有待发送指令、没有排队消息、也不在旁支对话里时,才允许之后中断时恢复原来的提示词。

数据流:进去没有参数;它读取 cancel_edit、底部输入框、输入队列、是否有后续消息、是否在侧边对话等状态;最后把 cancel_edit.armed 设成 true 或 false,表示中断时能不能取回那条输入。

调用关系:它是在 ChatWidget::record_cancel_edit_candidate 之后的安全检查。真正用到这个结果的是 ChatWidget::take_armed_cancel_edit_prompt,后者会在中断流程里尝试取出被保护的输入。

ChatWidget::take_armed_cancel_edit_prompt29–35 ↗
fn take_armed_cancel_edit_prompt(&mut self, reason: TurnAbortReason) -> Option<UserMessage>

作用:在用户打断当前轮次时,取出之前准备好的、可以恢复的输入内容。它只在“确实是中断”且恢复功能已上膛时才会给出内容。

数据流:进去的是 TurnAbortReason,也就是这轮为什么停止;它检查原因是不是 Interrupted、cancel_edit 是否 armed 和 eligible;如果条件都满足,就把保存的 prompt 从状态里拿走并返回,否则返回 None。

调用关系:它被 ChatWidget::on_interrupted_turn 调用,是中断恢复流程的第一步。它决定这次中断是普通提示用户,还是要把被取消的输入通过 RestoreCancelledTurn 发回应用事件系统。

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

ChatWidget::clear_cancel_edit37–39 ↗
fn clear_cancel_edit(&mut self)

作用:清空“取消后恢复输入”的所有临时记录。它相当于把这套小保险机制恢复到初始状态。

数据流:进去没有参数;它用默认值重建 CancelEditState;出来没有返回值,但 cancel_edit 里的 prompt、eligible、armed 等状态都会被重置。

调用关系:它会在不再需要保留取消编辑信息时使用。它调用默认构造逻辑,保证状态回到统一的干净起点。

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

ChatWidget::set_initial_user_message_submit_suppressed41–43 ↗
fn set_initial_user_message_submit_suppressed(&mut self, suppressed: bool)

作用:设置“初始用户消息是否暂时不要自动提交”。这用于某些启动或准备阶段,避免界面还没准备好就把第一条消息发出去。

数据流:进去的是一个布尔值 suppressed;函数把它写入 suppress_initial_user_message_submit;没有返回值,之后是否自动提交初始消息会受这个开关影响。

调用关系:它影响 ChatWidget::submit_initial_user_message_if_pending 的行为。外部流程可以先打开这个开关,等环境准备好了再关闭。

ChatWidget::submit_initial_user_message_if_pending45–56 ↗
fn submit_initial_user_message_if_pending(&mut self)

作用:如果启动时带了一条初始用户消息,并且现在允许发送,就把它提交出去。这样支持“打开界面后自动发送第一句话”的体验。

数据流:进去没有参数;它先看 suppress_initial_user_message_submit 是否禁止发送,在 Windows 或测试环境下还会看是否需要先做提权沙箱准备;如果都没挡住,就从 initial_user_message 里取出消息并调用提交逻辑。结果是这条初始消息被消费并发送,或继续留着等待。

调用关系:它通常在界面启动或准备完成后被调用。它受 ChatWidget::set_initial_user_message_submit_suppressed 控制,并把真正发送工作交给 submit_user_message。

ChatWidget::pop_next_queued_user_message58–96 ↗
fn pop_next_queued_user_message(
        &mut self,
    ) -> Option<(QueuedUserMessage, UserMessageHistoryRecord)>

作用:从输入队列里取出下一条该处理的用户消息。它会优先处理“被拒绝的 steer 指令”,也就是之前想插入当前轮次但服务器没接受的指令。

数据流:进去没有参数;如果没有被拒绝的 steer,就从 queued_user_messages 队头取一条,并配上对应历史记录;如果有被拒绝的 steer,就把它们全部取出、补齐历史记录、合并成一条消息返回。出来是一个可选的消息加历史记录,队列里的对应内容会被移除。

调用关系:它是队列消费入口之一。后续发送流程会用它拿下一条要处理的消息;当处理被拒绝 steer 时,它会把多条合成一条,避免零散内容乱序。

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

ChatWidget::pop_latest_queued_composer_state98–126 ↗
fn pop_latest_queued_composer_state(&mut self) -> Option<ThreadComposerState>

作用:取出最近排队的一条输入,并把它变成可以放回输入框的草稿状态。它适合做“撤回最近排队内容并重新编辑”。

数据流:进去没有参数;它先从普通排队消息的队尾取,如果没有,再从被拒绝 steer 的队尾取;取到后会拿出文字、图片、粘贴内容和历史记录,转换成 ThreadComposerState。出来是可选的输入框状态,同时对应队列项会被移除。

调用关系:它把队列里的消息还原成输入框能显示的格式,转换工作交给 ChatWidget::composer_state_from_user_message。它和 ChatWidget::restore_composer_state 常常可以连起来用:先取出状态,再塞回输入框。

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

ChatWidget::enqueue_rejected_steer128–143 ↗
fn enqueue_rejected_steer(&mut self) -> bool

作用:当服务器说“当前这轮不能再插入 steer 指令”时,把本来等待确认的那条指令放进被拒绝队列,稍后再处理。steer 可以理解为用户想在模型回答中途追加的指示。

数据流:进去没有参数;它从 pending_steers 队头取一条,如果没有可取的,就写一条警告并返回 false;如果取到了,就把消息和历史记录放进 rejected 队列,刷新待输入预览,返回 true。

调用关系:它发生在收到“当前轮次不可 steer”的错误之后。它不丢弃用户内容,而是把内容转移到 rejected_steers_queue,之后 ChatWidget::pop_next_queued_user_message 或恢复流程会继续处理。

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

ChatWidget::on_interrupted_turn149–198 ↗
fn on_interrupted_turn(&mut self, reason: TurnAbortReason)

作用:处理一次对话轮次被中断后的收尾。中断可能来自用户按 Esc、预算耗尽,或者审核完成;这个函数保证该提示的提示、该恢复的输入恢复、该发送的排队指令继续发送。

数据流:进去的是 TurnAbortReason;它先尝试取出可恢复的取消编辑内容,然后结束当前轮次、决定是否显示中断提示;如果设置了“中断后立刻发送 pending steer”,就把这些指令合并后提交,否则把待处理消息合并并恢复到输入框;最后刷新预览、可能发送 RestoreCancelledTurn 事件,并请求重绘界面。

调用关系:这是中断场景的中心调度函数。它调用 ChatWidget::take_armed_cancel_edit_prompt 判断有没有取消输入要恢复,调用 ChatWidget::drain_pending_messages_for_restore 收集待恢复内容,调用 ChatWidget::restore_composer_state 把内容放回输入框,并通过历史事件告诉用户发生了什么。

调用图:调用 3 个内部函数(drain_pending_messages_for_restore, restore_composer_state, take_armed_cancel_edit_prompt);外部调用 3 个(RestoreCancelledTurn, new_error_event, new_info_event)。

ChatWidget::drain_pending_messages_for_restore207–292 ↗
fn drain_pending_messages_for_restore(&mut self) -> Option<ThreadComposerState>

作用:把各种还没完成的输入收集起来,合并成一个可以放回输入框的草稿。它解决的问题是:中断后用户的排队消息、插队指令、当前输入框草稿不能散落在不同地方。

数据流:进去没有参数;它读取并清空 rejected steers、pending steers、queued user messages,还读取当前输入框草稿;然后按稳定顺序合并这些消息,并处理粘贴占位符撞名问题;出来是一个 ThreadComposerState,包含合并后的文字、图片、提及、粘贴内容等,如果没有东西可恢复就返回 None。

调用关系:它只在需要恢复待输入内容时被 ChatWidget::on_interrupted_turn 调用。它会调用 remap_colliding_paste_placeholders 来避免粘贴占位符冲突,再用 ChatWidget::composer_state_from_user_message 把合并结果变成输入框状态。

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

ChatWidget::restore_user_message_to_composer294–299 ↗
fn restore_user_message_to_composer(&mut self, user_message: UserMessage)

作用:把一条完整的用户消息直接放回输入框,方便用户继续编辑或重新发送。

数据流:进去的是 UserMessage;函数把它包装成 ThreadComposerState,粘贴待处理列表为空;然后调用 ChatWidget::restore_composer_state。出来没有返回值,但输入框会显示这条消息的内容。

调用关系:它是一个便捷入口:外部只要有 UserMessage,不必关心输入框内部状态格式。真正写入界面的动作交给 ChatWidget::restore_composer_state。

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

ChatWidget::restore_composer_state301–319 ↗
fn restore_composer_state(&mut self, composer: ThreadComposerState)

作用:把保存好的输入框状态真正写回界面。用户看到的文字、图片、远程图片链接、提及和粘贴内容,都会在这里恢复。

数据流:进去的是 ThreadComposerState;它拆出文字、图片、远程图片、文本元素、提及绑定和粘贴项;本地图片会转成路径,远程图片写入对应状态,文字和提及写入底部输入框,粘贴项也放回输入框。出来没有返回值,但界面输入区被更新。

调用关系:它是恢复输入的底层执行者。ChatWidget::on_interrupted_turn、ChatWidget::restore_thread_input_state、ChatWidget::restore_user_message_to_composer 都会把准备好的状态交给它落到界面上。

调用图:被 3 处调用(on_interrupted_turn, restore_thread_input_state, restore_user_message_to_composer)。

ChatWidget::composer_state_from_user_message321–340 ↗
fn composer_state_from_user_message(
        user_message: UserMessage,
        pending_pastes: Vec<(String, String)>,
    ) -> ThreadComposerState

作用:把 UserMessage 转成 ThreadComposerState。前者更像“要发送的消息”,后者更像“输入框里正在编辑的草稿”。

数据流:进去的是 UserMessage 和一组 pending_pastes;函数把消息里的文字、图片、远程图片、文本元素、提及绑定拆出来,再加上粘贴信息;出来是 ThreadComposerState。

调用关系:它是一个格式转换小工具,被恢复输入、取回排队草稿等流程使用。它本身不改界面,只把数据整理成 ChatWidget::restore_composer_state 能吃的形状。

ChatWidget::capture_thread_input_state342–385 ↗
fn capture_thread_input_state(&self) -> Option<ThreadInputState>

作用:给当前聊天线程的输入状态拍一张“快照”。切换线程、保存现场或之后恢复时,需要靠这张快照把输入框和队列原样带回来。

数据流:进去没有参数;它读取底部输入框草稿、pending steers、被拒绝队列、排队消息、协作模式、任务运行状态和模型轮次运行状态;出来是 Some(ThreadInputState),里面装着这些状态。如果输入框没内容,composer 字段会是 None,但队列和运行状态仍会记录。

调用关系:它和 ChatWidget::restore_thread_input_state 是一对:一个负责拍照,一个负责按照片复原。它不会清空或修改队列,只读取当前状态。

ChatWidget::restore_thread_input_state387–449 ↗
fn restore_thread_input_state(&mut self, input_state: Option<ThreadInputState>)

作用:把之前保存的线程输入快照恢复回来。它用于切回某个聊天线程时,让输入框、待发送队列、协作模式和运行标记看起来像从没离开过。

数据流:进去的是可选的 ThreadInputState;如果有状态,就恢复协作模式、运行状态、输入框内容、pending steers、被拒绝队列和普通排队消息,并补齐缺失的历史记录或比较键;如果没有状态,就清空输入队列并恢复空输入框。最后更新任务运行显示、刷新待输入预览并请求重绘。

调用关系:它是恢复线程现场的主入口。它调用 ChatWidget::restore_composer_state 把草稿写回输入框,用当前时间恢复运行生命周期,并在最后刷新界面相关提示,确保内部状态和用户看到的画面一致。

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

ChatWidget::set_queue_autosend_suppressed451–453 ↗
fn set_queue_autosend_suppressed(&mut self, suppressed: bool)

作用:设置排队消息是否禁止自动发送。这个开关能让外部流程临时按住队列,避免消息在不合适的时机自己发出去。

数据流:进去的是布尔值 suppressed;函数把它写入 input_queue.suppress_queue_autosend;没有返回值,之后队列自动发送逻辑会按这个开关行动。

调用关系:它是控制输入队列行为的小开关。其他流程在需要暂停或恢复自动发送时会调用它,而真正的发送流程会读取这个状态。

tui/src/chatwidget/input_submission.rs源码 ↗
orchestrationrequest handling

用户按下发送键后,事情并不只是“把文字丢出去”。这个文件就像聊天窗口的收发室:先把输入框里的文字、图片、远程图片链接、@提到的工具或应用整理成一份 UserMessage;再判断当前会话能不能接收、模型支不支持图片、是否以“!”开头要执行本机 shell 命令(也就是在用户电脑上跑的命令)。如果会话还没准备好,它会把消息放进队列,等以后再发;如果图片不被支持,它会把草稿完整放回输入框,避免用户重写。真正发送前,它还会把技能、插件、应用提及转成模型能理解的输入项,并记录历史,方便以后按上下键找回。

函数细节9
ChatWidget::user_message_from_submission6–22 ↗
fn user_message_from_submission(
        &mut self,
        text: String,
        text_elements: Vec<TextElement>,
    ) -> UserMessage

作用:把输入框这一次提交的内容打包成一份完整的用户消息。它不只拿文字,还会带上刚附加的本地图片、远程图片链接和提及绑定信息。

数据流:进去的是用户输入的文字和文字元素;它再从底部输入区取走最近提交的图片占位、远程图片地址和提及绑定;出来的是一个 UserMessage,后面的发送流程都靠这份打包好的消息继续走。

调用关系:这是发送流程的前置打包步骤。别的提交入口会先用它把输入框内容变成统一格式,然后再交给后面的 submit_user_message 系列函数判断和发送。

ChatWidget::submit_shell_command24–38 ↗
fn submit_shell_command(&mut self, command: &str) -> QueueDrain

作用:处理以 shell 命令形式提交的内容。shell 命令可以理解成让本机系统执行的一行命令,而不是发给聊天模型的话。

数据流:进去的是命令字符串;它先去掉前后空格,如果空了,就往历史区插入一条帮助提示并继续处理队列;如果不空,就生成“运行本机命令”的 AppCommand 提交出去,并告诉队列先停下来。

调用关系:它是 shell 命令的实际执行入口,由 ChatWidget::submit_shell_command_with_history 调用。它会用外部的 run_user_shell_command 生成命令,也会在空命令时通过历史单元显示帮助。

调用图:被 1 处调用(submit_shell_command_with_history);外部调用 4 个(new, run_user_shell_command, InsertHistoryCell, new_info_event)。

ChatWidget::submit_shell_command_with_history40–50 ↗
fn submit_shell_command_with_history(
        &mut self,
        command: &str,
        history_text: &str,
    ) -> QueueDrain

作用:在执行 shell 命令的同时,决定要不要把这条命令写进输入历史。这样用户以后可以找回自己运行过的命令。

数据流:进去的是实际要执行的命令和要写入历史的文本;它先交给 ChatWidget::submit_shell_command;如果命令真的被接受并让队列停止,就把历史文本追加到消息历史;最后返回队列该继续还是停止的结果。

调用关系:它包在 ChatWidget::submit_shell_command 外面,加了一层“记历史”的动作。排队的 shell 提示和普通用户消息里遇到“!”转义时,都会走到这里。

调用图:调用 1 个内部函数(submit_shell_command);被 2 处调用(submit_queued_shell_prompt, submit_user_message_with_history_and_shell_escape_policy)。

ChatWidget::submit_queued_shell_prompt52–63 ↗
fn submit_queued_shell_prompt(&mut self, user_message: UserMessage) -> QueueDrain

作用:处理之前排队等待的输入,尤其是判断它是不是以“!”开头的本机命令。它让队列里的内容在轮到自己时走正确路线。

数据流:进去的是一份排队中的 UserMessage;它查看文字是否以“!”开头,如果是,就去掉感叹号后当 shell 命令提交并记录历史;如果不是,就按普通聊天消息发送;出来的是队列继续或停止的指令。

调用关系:它位于输入队列被消费的时候。它可能把任务交给 ChatWidget::submit_shell_command_with_history,也可能交给 ChatWidget::submit_user_message。

调用图:调用 2 个内部函数(submit_shell_command_with_history, submit_user_message)。

ChatWidget::submit_user_message65–70 ↗
fn submit_user_message(&mut self, user_message: UserMessage)

作用:用最常见的方式发送一条普通用户消息。调用者不需要关心历史记录策略,它会默认把用户输入文本作为历史内容。

数据流:进去的是一份 UserMessage;它把消息和默认的历史记录方式一起交给更底层的提交函数;它不返回发送出的命令,只是触发发送流程。

调用关系:这是普通消息发送的简化入口。ChatWidget::submit_queued_shell_prompt 在确认不是 shell 命令时会调用它,它再把活交给 ChatWidget::submit_user_message_with_history_record。

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

ChatWidget::submit_user_message_with_history_record72–83 ↗
fn submit_user_message_with_history_record(
        &mut self,
        user_message: UserMessage,
        history_record: UserMessageHistoryRecord,
    ) -> bool

作用:发送用户消息,并允许调用者指定这条消息该怎样写入历史。它返回一个布尔值,告诉外面这次提交有没有被接受。

数据流:进去的是 UserMessage 和历史记录策略;它默认允许“!”这种 shell 转义,然后调用最完整的提交函数;出来的是是否接受提交的结果。

调用关系:它是普通发送入口和完整发送逻辑之间的中间层。ChatWidget::submit_user_message 会调用它,它再调用 ChatWidget::submit_user_message_with_history_and_shell_escape_policy。

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

ChatWidget::submit_user_message_with_shell_escape_policy85–96 ↗
fn submit_user_message_with_shell_escape_policy(
        &mut self,
        user_message: UserMessage,
        shell_escape_policy: ShellEscapePolicy,
    ) -> Option<AppCommand>

作用:发送用户消息时,允许调用者决定“!”开头是否能被当成本机命令。这个开关能防止某些场景下用户文字被误当命令执行。

数据流:进去的是 UserMessage 和 shell 转义策略;它使用默认的历史记录方式,把所有判断交给完整提交函数;出来的是可能生成的 AppCommand,如果没有真正发出命令或消息,就返回空。

调用关系:这是需要精细控制“!”行为的入口。它直接调用 ChatWidget::submit_user_message_with_history_and_shell_escape_policy,并把底层生成的 AppCommand 带回给调用者。

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

ChatWidget::submit_user_message_with_history_and_shell_escape_policy98–421 ↗
fn submit_user_message_with_history_and_shell_escape_policy(
        &mut self,
        user_message: UserMessage,
        history_record: UserMessageHistoryRecord,
        shell_escape_policy: ShellE

作用:这是本文件最核心的发送流程。它把用户输入从“草稿”变成应用能执行的命令,同时处理排队、空消息、图片限制、shell 命令、提及、历史记录和界面显示。

数据流:进去的是完整用户消息、历史记录方式和是否允许 shell 转义;它先检查会话是否准备好,没准备好就放进队列;再拒绝空消息,或在模型不支持图片时把草稿恢复到输入框;如果允许且文字以“!”开头,就改走本机命令;否则它把远程图片、本地图片、文字、技能、插件、应用提及整理成 UserInput 列表,选择当前模型和权限配置,生成 user_turn 类型的 AppCommand 并提交。成功后,它会写历史、记录待处理输入、更新聊天记录显示,并返回“已接受”和生成的命令。

调用关系:它是所有普通消息提交最终汇合的地方,由 ChatWidget::submit_user_message_with_history_record 和 ChatWidget::submit_user_message_with_shell_escape_policy 调用。它遇到 shell 输入会转给 ChatWidget::submit_shell_command_with_history,遇到不支持的图片会转给 ChatWidget::restore_blocked_image_submission;最终会生成并提交应用命令,把聊天回合交给后续系统处理。

调用图:调用 5 个内部函数(from, connector_mention_slug, restore_blocked_image_submission, submit_shell_command_with_history, from);被 2 处调用(submit_user_message_with_history_record, submit_user_message_with_shell_escape_policy);外部调用 8 个(new, new, new, new, run_user_shell_command, user_turn, format!, warn!)。

ChatWidget::restore_blocked_image_submission430–451 ↗
fn restore_blocked_image_submission(
        &mut self,
        text: String,
        text_elements: Vec<TextElement>,
        local_images: Vec<LocalImageAttachment>,
        mention_bindings: Vec<Me

作用:当用户附了图片,但当前模型不支持图片时,把这次草稿完整放回输入框。这样用户可以删掉图片或换模型后再试,不会丢文字和提及信息。

数据流:进去的是原来的文字、文字元素、本地图片、提及绑定和远程图片链接;它把远程图片地址放回组件状态,把文字、本地图片路径和提及绑定放回输入框;同时在聊天历史里加一条警告,并请求界面重画。

调用关系:它只在核心提交函数 ChatWidget::submit_user_message_with_history_and_shell_escape_policy 检测到“图片不能发”时被调用。它会创建警告历史单元,让用户明确知道为什么发送被挡住。

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

tui/src/chatwidget/input_flow.rs源码 ↗
orchestration用户输入处理、回合结束后继续发送队列、界面状态刷新时

聊天界面不是用户一敲回车就一定能马上发消息。比如会话还没准备好、上一轮回答还在跑、正在弹窗、或计划模式还在输出,这时硬发可能会打乱顺序。这个文件就像一个前台接待员:先把输入整理成一条用户消息,判断现在能不能办理;能办就提交,不能办就放进队列。等系统空下来,它再从队列里取下一条,只取一条开始新的回合,避免多条消息挤在一起。它还会更新底部提示,让用户看到还有哪些输入在等着。里面也处理一些特殊情况,比如斜杠命令、服务档位命令、用户自己触发的 shell 命令,以及切换协作模式时不能打断正在运行的回合。

函数细节10
ChatWidget::handle_composer_input_result10–70 ↗
fn handle_composer_input_result(
        &mut self,
        input_result: InputResult,
        had_modal_or_popup: bool,
    )

作用:这是用户在输入框里提交内容后的总入口。它看这次输入到底是普通消息、排队消息、命令,还是其实什么都没发生,然后决定下一步怎么走。

数据流:进去的是输入框给出的结果,以及刚才是否有弹窗或模态框。它把提交的文字和附带内容整理成用户消息,空消息直接丢掉;能立即发送就提交,不能就放进队列;遇到命令就转给对应的命令处理。出来时没有直接返回重要结果,但它可能改变消息队列、状态标题、推理缓存和底部提示,并在弹窗关闭后尝试发送下一条队列消息。

调用关系:它是这个文件里最靠前的分流点。普通消息可能交给 ChatWidget::queue_user_message 或 ChatWidget::queue_user_message_with_options;如果系统刚从弹窗状态恢复,它会调用 ChatWidget::maybe_send_next_queued_input 继续处理积压输入;发送前还会用 ChatWidget::only_user_shell_commands_running 判断是否该先排队。

调用图:调用 4 个内部函数(maybe_send_next_queued_input, only_user_shell_commands_running, queue_user_message, queue_user_message_with_options);外部调用 1 个(from)。

ChatWidget::queue_user_message72–74 ↗
fn queue_user_message(&mut self, user_message: UserMessage)

作用:这是“把普通用户消息放入队列”的简化入口。调用者不用关心额外选项,它默认按普通消息处理。

数据流:进去的是一条已经整理好的用户消息。它给这条消息配上默认的排队动作,也就是普通发送,并带一个空的待处理粘贴列表,然后转交给更完整的排队函数。出来时它本身不返回值,但消息可能被加入队列,也可能因为当前可以发送而被直接提交。

调用关系:它被 ChatWidget::handle_composer_input_result 和 ChatWidget::submit_user_message_with_mode 使用,适合不需要特殊排队行为的场景。真正判断要排队还是发送的工作交给 ChatWidget::queue_user_message_with_options。

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

ChatWidget::set_queue_submissions_until_session_configured76–79 ↗
fn set_queue_submissions_until_session_configured(&mut self, queue: bool)

作用:这个函数控制一个开关:会话还没配置好时,用户提交的内容是否先排队。这样可以防止消息在系统没准备好时被误发。

数据流:进去的是一个布尔值,表示是否希望开启“先排队”模式。它再结合当前会话是否已经配置好,告诉底部输入区域要不要拦住提交。出来时没有返回值,但界面的提交行为会被调整。

调用关系:它不负责真正发送消息,而是在更早的界面状态阶段设置规则。后面用户真的提交时,ChatWidget::handle_composer_input_result 和排队相关函数会按这个状态工作。

ChatWidget::queue_user_message_with_options81–102 ↗
fn queue_user_message_with_options(
        &mut self,
        user_message: UserMessage,
        action: QueuedInputAction,
        pending_pastes: Vec<(String, String)>,
    )

作用:这是排队用户消息的核心函数。它不只是存文字,还会记住这条消息之后该怎么处理,比如普通发送、当斜杠命令解析,或当 shell 命令运行。

数据流:进去的是用户消息、排队动作,以及还没处理完的粘贴内容。它先看会话是否准备好、当前是否已有用户回合在等待或运行;如果不能马上发,就把消息和附加信息放进队列,并记录一条历史占位,再刷新底部预览;如果现在空闲,就直接提交这条消息。出来时没有返回值,但队列、历史记录和界面预览可能变化。

调用关系:ChatWidget::handle_composer_input_result 会在需要精细排队时直接调用它,ChatWidget::queue_user_message 也会把普通消息转到这里。它内部依赖 ChatWidget::is_user_turn_pending_or_running 判断忙不忙,并用 ChatWidget::refresh_pending_input_preview 更新用户能看到的队列提示。

调用图:调用 2 个内部函数(is_user_turn_pending_or_running, refresh_pending_input_preview);被 2 处调用(handle_composer_input_result, queue_user_message)。

ChatWidget::maybe_send_next_queued_input105–144 ↗
fn maybe_send_next_queued_input(&mut self) -> bool

作用:这个函数在系统空下来时尝试从队列里发出下一条输入。它一次只启动一条,避免积压消息一股脑冲出去。

数据流:进去时不需要额外参数,它读取队列、自动发送开关和当前是否有回合在运行。若禁止自动发送或系统正忙,它直接返回 false;否则它不断取队列头部,按动作分别走普通提交、斜杠提示提交或 shell 提交,遇到需要停下的情况就停止。出来的是一个布尔值,表示这次是否真的启动了后续提交,同时它会刷新剩余队列的底部预览。

调用关系:它通常由 ChatWidget::handle_composer_input_result 在弹窗关闭后调用,用来把之前被拦住的输入继续往前推进。它反复询问 ChatWidget::is_user_turn_pending_or_running,并在结束前调用 ChatWidget::refresh_pending_input_preview,让界面显示最新排队情况。

调用图:调用 2 个内部函数(is_user_turn_pending_or_running, refresh_pending_input_preview);被 1 处调用(handle_composer_input_result)。

ChatWidget::is_user_turn_pending_or_running146–148 ↗
fn is_user_turn_pending_or_running(&self) -> bool

作用:这个小函数回答一个关键问题:现在是不是已经有用户回合在等待开始,或者底部任务还在运行。很多地方靠它决定能不能再发下一条。

数据流:进去不需要参数,它读取输入队列里的“用户回合等待开始”标记,以及底部面板的任务运行状态。它把这两个信息合成一个真假结果:只要有一个说明系统忙,就返回 true;都没有才返回 false。它不改动任何状态。

调用关系:ChatWidget::queue_user_message_with_options 用它判断新消息该排队还是提交;ChatWidget::maybe_send_next_queued_input 用它判断队列能不能继续往外放消息。它是排队流程里的交通灯。

调用图:被 2 处调用(maybe_send_next_queued_input, queue_user_message_with_options)。

ChatWidget::only_user_shell_commands_running150–157 ↗
fn only_user_shell_commands_running(&self) -> bool

作用:这个函数判断当前是不是只有用户自己启动的 shell 命令还在跑。shell 命令可以理解成让电脑执行的一条系统命令,比如列目录或跑脚本。

数据流:进去不需要参数,它读取当前回合是否在运行、正在运行的命令列表,以及每个命令的来源。只有在代理回合还没结束、命令列表不空、并且所有命令都来自用户 shell 时,它才返回 true。它不修改数据。

调用关系:ChatWidget::handle_composer_input_result 会用它处理一个细节:如果只有用户 shell 命令还在跑,普通新消息先排队,避免和命令输出混在一起;但以感叹号开头的特殊命令可以走别的路径。

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

ChatWidget::refresh_pending_input_preview160–167 ↗
fn refresh_pending_input_preview(&mut self)

作用:这个函数刷新底部区域的“待发送输入”预览。用户能因此看见还有哪些消息或操作排着队。

数据流:进去不需要参数,它从输入队列生成一份预览,里面包括排队消息、等待中的引导内容和被拒绝的引导内容。然后它把这些信息交给底部面板显示。出来时没有返回值,但界面上的待处理提示会更新。

调用关系:ChatWidget::queue_user_message_with_options 在新增排队消息后调用它,ChatWidget::maybe_send_next_queued_input 在取走一条或多条队列内容后也调用它。它让内部队列状态和用户看到的界面保持一致。

调用图:被 2 处调用(maybe_send_next_queued_input, queue_user_message_with_options)。

ChatWidget::submit_user_message_with_mode169–201 ↗
fn submit_user_message_with_mode(
        &mut self,
        text: String,
        mut collaboration_mode: CollaborationModeMask,
    )

作用:这个函数用于带着指定协作模式发送一条文字消息。协作模式可以理解成“这次聊天按哪种工作方式来”,比如计划模式。

数据流:进去的是消息文字和一个协作模式设置。它先在计划模式下补上配置里的推理强度;如果当前已有回合运行,并且用户想切到另一个模式,它会显示错误并停止。否则它更新当前协作模式,把文字包装成一条没有图片和额外元素的用户消息;如果计划内容正在界面里流式输出,就先排队,否则直接提交。出来时没有返回值,但可能改变协作模式、显示错误、排队消息或提交消息。

调用关系:它是外部想“按某种模式发消息”时用的入口。需要排队时它调用 ChatWidget::queue_user_message;普通排队之后的具体判断仍由队列流程接手。

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

ChatWidget::queued_user_message_texts204–216 ↗
fn queued_user_message_texts(&self) -> Vec<String>

作用:这是测试用的小工具,用来拿到当前排队消息的文字列表。它方便测试检查队列里到底排了哪些内容。

数据流:进去不需要参数,它读取被拒绝的引导队列和普通用户消息队列,把每条消息的文字拷贝出来并合成一个列表。出来的是字符串列表,不会改变队列本身。

调用关系:它只在测试编译时存在,不参与正常用户操作。测试代码可以用它观察排队结果,从而确认 ChatWidget::queue_user_message_with_options 和 ChatWidget::maybe_send_next_queued_input 这类流程有没有按预期改变队列。

tui/src/chatwidget/interaction.rs源码 ↗
orchestrationmain loop / key event handling

聊天界面不是只有一个输入框。它可能有弹窗、选择列表、外部编辑器、正在运行的回答、排队的消息、图片附件、退出提示等。这个文件就是 ChatWidget 的交互路由层:先判断当前谁最该收到按键,再把事情交给对应部件。比如有弹窗时,大多数键先给底部面板;按 Ctrl+C 时可能是取消任务,也可能是第二次确认退出;按 Ctrl+V/Alt+V 时可能把剪贴板图片存成临时 PNG 再附加到草稿。它还处理复制上一条助手回复、重命名会话、粘贴大段文本时延迟刷新等细节。重要的是,它不直接完成所有 UI 工作,而是像调度员一样调用 bottom_pane、transcript、app_event_tx 等零件,保证用户的每个动作都落到正确地方,避免误退出、误清空或在不支持图片的模型里乱加附件。

函数细节25
ChatWidget::handle_key_event6–172 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)

作用:这是聊天窗口处理键盘输入的总入口。用户每按一次键,它都决定这个键该用来输入文字、操作弹窗、取消任务、退出、复制回复、粘贴图片,还是切换某些模式。

数据流:进去的是一个按键事件,包括按了哪个键、有没有 Ctrl 或 Alt、是不是按下动作。它先看底部面板是否有活动视图,再逐项检查特殊快捷键,例如 Ctrl+C、Ctrl+D、复制上一条回复、粘贴图片、编辑排队消息、打断当前任务、关闭提示、插件弹窗和协作模式。最后如果没有被这些规则吃掉,就把按键交给底部面板,并根据输入结果刷新界面或继续后续动作。

调用关系:它是本文件的核心分发器。它会在需要时调用 ChatWidget::attach_image、ChatWidget::copy_last_agent_markdown、ChatWidget::on_ctrl_c 和 ChatWidget::on_ctrl_d;其它普通输入则交给 bottom_pane,再让后续的 composer 处理流程接着做。

调用图:调用 5 个内部函数(attach_image, copy_last_agent_markdown, on_ctrl_c, on_ctrl_d, ctrl);外部调用 7 个(Char, interrupt, format!, new_error_event, matches!, debug!, warn!)。

ChatWidget::attach_image178–189 ↗
fn attach_image(&mut self, path: PathBuf)

作用:把一张本地图片加到当前聊天草稿里。它会先确认当前模型能不能看图片,避免用户发出去后模型根本处理不了。

数据流:进去的是图片文件路径。函数先检查当前模型是否支持图片输入;不支持时,在聊天历史里放一条警告并要求重画界面。支持时,把路径交给底部输入区作为附件,并刷新界面。

调用关系:它通常由 ChatWidget::handle_key_event 在用户用快捷键粘贴图片成功后调用。真正显示和保存附件的活儿交给 bottom_pane。

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

ChatWidget::composer_text_with_pending191–193 ↗
fn composer_text_with_pending(&self) -> String

作用:取出输入框里的文字,并把还在等待合并的输入也算进去。外部代码想知道“用户现在准备发送的完整文本”时会用它。

数据流:它不接收额外输入,只读取底部面板里的输入框状态。底部面板返回当前文本加上未落定的待处理文本,函数原样返回这个字符串。

调用关系:它是一个查询口,把 ChatWidget 外部想问的问题转交给 bottom_pane,不自己改任何状态。

ChatWidget::apply_external_edit195–199 ↗
fn apply_external_edit(&mut self, text: String)

作用:把外部编辑器改好的文字放回聊天输入框。比如用户用系统编辑器写了一大段内容,回来后就靠它同步到界面里。

数据流:进去的是外部编辑器返回的一段文本。它把文本交给底部面板更新输入框,然后刷新计划模式提示,并请求界面重画。

调用关系:它连接外部编辑器和聊天输入框。实际更新文字由 bottom_pane 做,ChatWidget 负责顺手刷新相关提示。

ChatWidget::external_editor_state201–203 ↗
fn external_editor_state(&self) -> ExternalEditorState

作用:读取外部编辑器当前状态。外部流程可以用它判断编辑器是不是正在打开、等待返回,或处在其它状态。

数据流:它不接收输入,只读取 ChatWidget 保存的 external_editor_state 字段,然后把这个状态返回。

调用关系:它是状态查询口,配合 ChatWidget::set_external_editor_state 使用,让外部编辑器流程能和聊天界面保持一致。

ChatWidget::set_external_editor_state205–207 ↗
fn set_external_editor_state(&mut self, state: ExternalEditorState)

作用:更新外部编辑器状态。这样聊天界面知道外部编辑器现在走到哪一步了。

数据流:进去的是一个 ExternalEditorState 状态值。函数把它写入 ChatWidget 自己保存的 external_editor_state 字段,不返回数据。

调用关系:它通常由外部编辑器相关流程调用,和 ChatWidget::external_editor_state 组成一读一写的状态接口。

ChatWidget::show_selection_view213–217 ↗
fn show_selection_view(&mut self, params: SelectionViewParams)

作用:在底部区域打开一个选择界面。用户需要从一组选项里挑一个时,会用到它。

数据流:进去的是选择界面的参数,比如标题、选项和行为。函数把参数交给底部面板显示,然后刷新计划模式提示,并请求重画界面。

调用关系:它负责把“显示选择器”这个需求接到 bottom_pane 上,同时让 ChatWidget 的其它提示状态跟着更新。

ChatWidget::no_modal_or_popup_active219–221 ↗
fn no_modal_or_popup_active(&self) -> bool

作用:判断当前有没有弹窗或模态界面挡在前面。模态界面就是那种必须先处理完,才能回到普通聊天的界面。

数据流:它不接收输入,只询问底部面板当前是否没有弹窗或模态界面。然后返回 true 或 false。

调用关系:很多快捷键规则都需要这个判断。这里把查询转交给 bottom_pane,让调用方不用知道底部面板内部怎么记录弹窗。

ChatWidget::can_launch_external_editor223–225 ↗
fn can_launch_external_editor(&self) -> bool

作用:判断现在能不能打开外部编辑器。这样可以避免在弹窗、任务或不合适的输入状态下强行打开。

数据流:它不接收输入,只向底部面板询问当前条件是否允许启动外部编辑器,然后返回布尔结果。

调用关系:它是外部编辑器启动前的守门口。具体规则由 bottom_pane 判断,ChatWidget 只提供统一入口。

ChatWidget::can_run_ctrl_l_clear_now227–238 ↗
fn can_run_ctrl_l_clear_now(&mut self) -> bool

作用:判断现在能不能用 Ctrl+L 清屏。规则很简单:任务正在跑时不允许清,免得用户误以为任务状态也被清掉了。

数据流:它读取底部面板是否有任务正在运行。没有任务时返回 true;有任务时往历史里加一条错误提示,要求界面重画,然后返回 false。

调用关系:它把 Ctrl+L 的限制做成一个单独检查。需要执行清屏前,外部流程可以先问它;错误提示通过 history_cell 创建并显示在聊天历史里。

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

ChatWidget::copy_last_agent_markdown241–243 ↗
fn copy_last_agent_markdown(&mut self)

作用:把上一条助手回复的原始 Markdown 文本复制到系统剪贴板。Markdown 可以理解为带简单格式标记的纯文本。

数据流:它不接收输入,只把默认的系统剪贴板复制函数传给内部实现。最终结果可能是复制成功、复制失败,或没有可复制内容。

调用关系:它由 ChatWidget::handle_key_event 在复制快捷键被按下时调用。真正的判断和提示由 ChatWidget::copy_last_agent_markdown_with 完成。

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

ChatWidget::truncate_agent_copy_history_to_user_turn_count245–251 ↗
fn truncate_agent_copy_history_to_user_turn_count(
        &mut self,
        user_turn_count: usize,
    )

作用:按用户发言轮数裁剪可复制的助手回复历史。这样回滚或删掉旧对话后,不会还能复制到已经不该存在的回复。

数据流:进去的是要保留到的用户发言数量。函数把这个数量交给 transcript,让它裁掉超出范围的复制历史。

调用关系:它是 ChatWidget 给 transcript 的一个安全同步入口,通常用于对话被回退或历史被修剪之后。

ChatWidget::copy_last_agent_markdown_with254–281 ↗
fn copy_last_agent_markdown_with(
        &mut self,
        copy_fn: impl FnOnce(&str) -> Result<Option<crate::clipboard_copy::ClipboardLease>, String>,
    )

作用:这是复制上一条助手回复的内部版本,可以替换剪贴板函数,方便测试。它还负责给用户显示成功或失败原因。

数据流:进去的是一个复制函数。它读取 transcript 里保存的上一条助手 Markdown:有内容就调用复制函数,成功后保存可能需要保活的剪贴板租约并写入成功提示;失败就写入错误提示;如果没有内容,或内容因回滚被清掉,也写入对应错误提示。最后请求重画界面。

调用关系:ChatWidget::copy_last_agent_markdown 会调用它并传入真实剪贴板。测试代码也可以传入假的复制函数,检查各种成功失败情况。

调用图:被 1 处调用(copy_last_agent_markdown);外部调用 3 个(format!, new_error_event, new_info_event)。

ChatWidget::last_agent_markdown_text284–286 ↗
fn last_agent_markdown_text(&self) -> Option<&str>

作用:在测试里读取上一条助手回复的 Markdown 文本。它让测试能确认复制相关状态是不是正确。

数据流:它不接收输入,只从 transcript 的 last_agent_markdown 字段取出文本引用;如果没有保存内容,就返回空。

调用关系:这是只在测试配置下存在的辅助口,主要服务复制历史相关测试,不参与普通运行流程。

ChatWidget::show_rename_prompt288–316 ↗
fn show_rename_prompt(&mut self)

作用:弹出给当前会话命名或改名的小输入框。用户输入名字并回车后,它会把新名字发给应用层保存。

数据流:它先检查当前是否允许改名;允许时读取已有会话名,决定标题是“命名”还是“重命名”,创建一个自定义输入框。用户提交后,回调会清理名字:空名字会插入错误消息,合法名字会通过事件发送器设置线程名。

调用关系:它先调用 ChatWidget::ensure_thread_rename_allowed 做权限检查,然后创建 CustomPromptView,并把这个视图交给 bottom_pane 显示。

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

ChatWidget::ensure_thread_rename_allowed318–326 ↗
fn ensure_thread_rename_allowed(&mut self) -> bool

作用:检查当前会话能不能改名。如果不能,它会把原因显示给用户。

数据流:它读取 thread_rename_block_message。如果里面有阻止原因,就加入错误消息并返回 false;如果没有阻止原因,就返回 true。

调用关系:ChatWidget::show_rename_prompt 在打开改名框前会先调用它。它像门卫一样,挡住不该出现的改名操作。

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

ChatWidget::handle_paste328–331 ↗
fn handle_paste(&mut self, text: String)

作用:处理用户粘贴进来的普通文本。它把文本放进输入区,并刷新相关提示。

数据流:进去的是粘贴的字符串。函数交给底部面板处理粘贴内容,然后刷新计划模式提示。

调用关系:它是粘贴文本进入 ChatWidget 的入口。具体怎么合并到输入框由 bottom_pane 负责。

ChatWidget::handle_paste_burst_tick334–350 ↗
fn handle_paste_burst_tick(&mut self, frame_requester: FrameRequester) -> bool

作用:处理“连续大量粘贴”的节奏控制。它避免每来一点粘贴内容就立刻重画界面,减少闪烁和浪费。

数据流:进去的是一个 FrameRequester,也就是请求未来某个时间再画一帧的工具。函数先问底部面板是否该把一批粘贴内容一次性刷出来;如果刷了,就刷新提示、请求立即重画并告诉调用方跳过当前帧。若仍在收集粘贴爆发,就安排稍后再检查并跳过当前帧。若没有粘贴爆发,就返回 false。

调用关系:它和 bottom_pane 的粘贴缓冲机制配合。需要延迟时会调用 schedule_frame_in,并使用 ChatComposer::recommended_paste_flush_delay 提供的推荐等待时间。

调用图:调用 2 个内部函数(recommended_paste_flush_delay, schedule_frame_in)。

ChatWidget::on_ctrl_c360–402 ↗
fn on_ctrl_c(&mut self)

作用:专门处理 Ctrl+C。这个键很敏感:它可能是关闭弹窗、取消正在跑的任务,也可能是确认退出程序。

数据流:它先构造 Ctrl+C 这个按键标识,并询问底部面板能否自己处理。若底部面板已处理,就按情况设置或清除“双击退出”提示。若没有处理,它根据配置判断:没有双击退出时,有任务就打断任务,没任务就直接退出;启用双击退出时,第二次在有效时间内按同一个快捷键才退出,第一次只显示提示。打断任务前还可能暂停当前目标状态。

调用关系:它由 ChatWidget::handle_key_event 在收到 Ctrl+C 时调用。内部会用 ChatWidget::arm_quit_shortcut、ChatWidget::quit_shortcut_active_for、ChatWidget::is_cancellable_work_active 和 ChatWidget::pause_active_goal_for_interrupt 来完成退出或打断流程。

调用图:调用 5 个内部函数(arm_quit_shortcut, is_cancellable_work_active, pause_active_goal_for_interrupt, quit_shortcut_active_for, ctrl);被 1 处调用(handle_key_event);外部调用 2 个(Char, interrupt_and_restore_prompt_if_no_output)。

ChatWidget::on_ctrl_d408–433 ↗
fn on_ctrl_d(&mut self) -> bool

作用:专门处理 Ctrl+D 退出快捷键。它只在输入框为空、没有弹窗时参与退出,避免用户正在编辑内容时误退出。

数据流:它先构造 Ctrl+D 这个按键标识。若未启用双击退出,只有输入框为空且没有弹窗时才直接请求退出,否则返回 false 让别人处理。若启用双击退出,第二次有效按下会退出;第一次在条件合适时只设置退出提示并返回 true。

调用关系:它由 ChatWidget::handle_key_event 在收到 Ctrl+D 时调用。它依赖 ChatWidget::quit_shortcut_active_for 判断是否是第二次确认,也会调用 ChatWidget::arm_quit_shortcut 显示提示。

调用图:调用 3 个内部函数(arm_quit_shortcut, quit_shortcut_active_for, ctrl);被 1 处调用(handle_key_event);外部调用 1 个(Char)。

ChatWidget::quit_shortcut_active_for436–441 ↗
fn quit_shortcut_active_for(&self, key: KeyBinding) -> bool

作用:判断某个退出快捷键是不是已经被“预备好”,而且还没过期。它用于双击退出逻辑。

数据流:进去的是一个按键绑定。函数比较它是否等于当前记录的 quit_shortcut_key,再检查当前时间是否还早于过期时间。两项都满足才返回 true。

调用关系:ChatWidget::on_ctrl_c 和 ChatWidget::on_ctrl_d 都会调用它,用来判断这次按键是不是确认退出的第二下。

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

ChatWidget::arm_quit_shortcut448–454 ↗
fn arm_quit_shortcut(&mut self, key: KeyBinding)

作用:启动双击退出的第一步:记住这次按的是哪个退出键,并在底部显示“再按一次退出”的提示。

数据流:进去的是一个按键绑定。函数用当前时间加上固定超时时间,保存过期时刻和按键本身,然后让底部面板显示对应提示。

调用关系:ChatWidget::on_ctrl_c 和 ChatWidget::on_ctrl_d 在需要等待第二次确认时调用它。状态保存在 ChatWidget,提示显示交给 bottom_pane。

调用图:被 2 处调用(on_ctrl_c, on_ctrl_d);外部调用 1 个(now)。

ChatWidget::is_cancellable_work_active457–459 ↗
fn is_cancellable_work_active(&self) -> bool

作用:判断现在有没有可以被取消的工作。这里包括正在运行的任务,也包括 review 模式,因为 review 里 Ctrl+C 应该先取消而不是退出。

数据流:它读取底部面板是否有任务运行,以及 review 是否处于 review 模式。只要其中一个为真,就返回 true。

调用关系:ChatWidget::on_ctrl_c 用它决定 Ctrl+C 应该打断当前工作,还是走退出流程。

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

ChatWidget::pause_active_goal_for_interrupt461–479 ↗
fn pause_active_goal_for_interrupt(&self)

作用:在用户打断正在运行的助手回合时,把当前目标标成暂停。这样应用层知道这个目标不是自然完成,而是被用户中断了。

数据流:它不接收输入,只读取当前是否有助手回合在运行、当前目标状态是否活跃、以及是否有线程 ID。条件都满足时,通过事件发送器发出 SetThreadGoalStatus,把该线程目标状态改成 Paused。

调用关系:ChatWidget::on_ctrl_c 在准备提交打断命令前会调用它。真正更新线程目标状态的工作交给 app_event_tx 发出的应用事件处理。

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

命令和上下文功能

路由 slash 命令,并支持用于丰富或专门化聊天输入的上下文命令功能。

tui/src/chatwidget/skills.rs源码 ↗
domain_logic用户打开技能菜单、收到技能列表、编辑聊天输入、关闭管理弹窗时活跃

这个文件解决的是聊天界面里“技能怎么出现、怎么选择、怎么识别”的问题。技能可以理解成一张张能力卡片,用户在聊天里用特殊符号提到它,系统就知道这次对话要带上对应能力。文件前半部分挂在 ChatWidget 上,负责打开技能菜单、显示启用开关、记录用户改了哪些技能,并把可用技能交给提及系统。后半部分负责读用户输入:从文字里找出像“$name”或带路径的链接提及,再判断它指向技能还是应用。它还会避开常见环境变量,比如 PATH、HOME,防止把普通技术文本误当成技能。这里还做了一层数据翻译,把应用服务器给的技能格式,转成核心技能系统能理解的格式。

函数细节25
ChatWidget::open_skills_list27–33 ↗
fn open_skills_list(&mut self)

作用:这个函数用来快速打开技能提及列表。它不是直接弹窗,而是在输入框里插入触发符号,让已有的自动补全机制接手。

数据流:进去的是当前聊天窗口自身,它读取功能开关 MentionsV2 是否开启;如果开启,就往输入框插入 @,否则插入 $;出来的结果是输入框内容变了,后续界面会根据这个符号显示技能候选。

调用关系:它通常由“打开技能列表”的事件触发。它不自己准备列表,只负责按当前配置放入正确的入口符号,把后面的工作交给输入框的提及补全流程。

ChatWidget::open_skills_menu35–71 ↗
fn open_skills_menu(&mut self)

作用:这个函数打开一个小菜单,让用户选择是查看技能列表,还是进入启用和禁用技能的管理页。它相当于技能功能的总入口。

数据流:进去的是当前聊天窗口,它先看 MentionsV2 开关来决定提示用户按 @ 还是 $;然后创建两个菜单项;最后把这些菜单项交给底部面板显示。用户选择菜单项后,会发送对应的 AppEvent 事件。

调用关系:它调用 standard_popup_hint_line 来生成统一的底部按键提示。菜单里的两个动作不会在这里直接执行,而是发送 OpenSkillsList 或 OpenManageSkillsPopup 事件,让外层事件循环再去调用对应流程。

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

ChatWidget::open_manage_skills_popup73–110 ↗
fn open_manage_skills_popup(&mut self)

作用:这个函数打开“启用/禁用技能”的弹窗。用户可以在这里像开关灯一样打开或关闭某个技能。

数据流:进去的是当前聊天窗口,它读取已经加载的 skills_all;如果没有技能,就在聊天里提示“没有可用技能”;如果有,就先保存每个技能原来的开关状态,再把服务器格式的技能转成核心格式,整理出名称、说明、路径和是否启用,最后创建 SkillsToggleView 并显示在底部面板。

调用关系:它会用 protocol_skill_to_core 把技能资料翻译成界面和核心系统都能懂的样子,并用 SkillsToggleView::new 创建开关列表。这个弹窗后续的开关变化会走 ChatWidget::update_skill_enabled,关闭时会走 ChatWidget::handle_manage_skills_closed。

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

ChatWidget::update_skill_enabled112–119 ↗
fn update_skill_enabled(&mut self, path: AbsolutePathBuf, enabled: bool)

作用:这个函数在用户切换某个技能开关时更新内存里的状态。它保证聊天输入里的技能提及候选也随之刷新。

数据流:进去的是一个技能文件路径和新的启用状态;它在 skills_all 里找到路径相同的技能,把 enabled 改成新值;然后重新算出所有已启用技能,并交给 set_skills 更新提及系统。

调用关系:它由技能管理弹窗里的开关动作触发。它调用 enabled_skills_for_mentions,把完整技能列表过滤成“当前可被提及的技能列表”,再交给 ChatWidget 的技能设置流程。

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

ChatWidget::handle_manage_skills_closed121–152 ↗
fn handle_manage_skills_closed(&mut self)

作用:这个函数在技能管理弹窗关闭后,告诉用户刚才改了多少个技能。它让用户知道自己的操作有没有生效。

数据流:进去的是当前聊天窗口,它取出打开弹窗时保存的初始状态,再读取现在的技能状态;逐个比较后统计新增启用和新增禁用的数量;如果没有变化就什么都不说,如果有变化就在聊天里加一条信息提示。

调用关系:它接在管理弹窗关闭之后执行。它依赖 open_manage_skills_popup 之前保存的 skills_initial_state;如果没有这份初始状态,就说明不是一次正常的管理流程,函数会直接返回。

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

ChatWidget::set_skills_from_response154–158 ↗
fn set_skills_from_response(&mut self, response: &SkillsListResponse)

作用:这个函数接收服务器返回的技能列表,并把属于当前工作目录的那一份装进聊天窗口。它让界面看到的技能和当前项目位置保持一致。

数据流:进去的是 SkillsListResponse,也就是服务器返回的多组技能数据;它用当前配置里的 cwd,也就是当前工作目录,挑出对应的技能;然后保存到 skills_all;最后筛出已启用技能并更新提及候选。

调用关系:它调用 skills_for_cwd 做“按当前目录挑技能”,再调用 enabled_skills_for_mentions 做“只保留启用技能”。通常它会在应用服务器返回技能列表后被调用。

调用图:调用 2 个内部函数(enabled_skills_for_mentions, skills_for_cwd)。

ChatWidget::annotate_skill_reads_in_parsed_cmd160–187 ↗
fn annotate_skill_reads_in_parsed_cmd(
        &self,
        mut parsed_cmd: Vec<ParsedCommand>,
    ) -> Vec<ParsedCommand>

作用:这个函数给命令记录里的技能文件读取操作加上更友好的说明。比如原本只显示读了 SKILL.md,它会补成读了某某技能的 SKILL.md。

数据流:进去的是一组已经解析好的命令;它逐条查看其中的 Read 读文件命令,只关心文件名叫 SKILL.md 的项;如果路径正好匹配已加载技能,就把显示名改成“SKILL.md(某技能)”;最后返回改过的命令列表。

调用关系:它不改变真实读文件行为,只改变展示文字。它通常用于把底层命令历史展示给用户前,让信息更容易看懂。

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

skills_for_cwd190–199 ↗
fn skills_for_cwd(
    cwd: &AbsolutePathBuf,
    skills_entries: &[SkillsListEntry],
) -> Vec<ProtocolSkillMetadata>

作用:这个函数从服务器返回的多份技能列表里,挑出当前目录对应的那一份。因为不同项目目录可能有不同技能。

数据流:进去的是当前工作目录 cwd 和一批 SkillsListEntry;它逐个比较 entry.cwd 和 cwd;找到匹配项就复制其中的 skills 返回;找不到就返回空列表。

调用关系:它被 ChatWidget::set_skills_from_response 调用,是“服务器大包数据”进入聊天窗口前的筛选步骤。

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

enabled_skills_for_mentions201–207 ↗
fn enabled_skills_for_mentions(skills: &[ProtocolSkillMetadata]) -> Vec<SkillMetadata>

作用:这个函数把完整技能列表缩小成“用户当前允许提及的技能”。没有启用的技能不会出现在后续提及候选里。

数据流:进去的是服务器协议格式的技能列表;它先过滤掉 enabled 为 false 的技能,再把剩下的技能转换成核心技能格式;转换失败的会被跳过;出来的是可供提及系统使用的 SkillMetadata 列表。

调用关系:它被 ChatWidget::set_skills_from_response 和 ChatWidget::update_skill_enabled 调用。前者在首次加载技能后使用,后者在用户切换开关后使用。

调用图:被 2 处调用(set_skills_from_response, update_skill_enabled);外部调用 1 个(iter)。

protocol_skill_to_core209–255 ↗
fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> Option<SkillMetadata>

作用:这个函数负责把应用服务器发来的技能资料,翻译成核心技能系统使用的格式。可以把它理解成两个部门之间的表格转换器。

数据流:进去的是 ProtocolSkillMetadata;它先把 scope 字段通过 JSON 转换成核心系统认识的类型,如果失败就记录警告并返回 None;成功后会复制名称、说明、界面信息、依赖工具和技能文件路径,组装成 SkillMetadata 返回。

调用关系:它在打开管理弹窗和生成启用技能列表时都会用到。它把外部协议层的数据隔离开,避免界面和核心逻辑到处都知道服务器字段长什么样。

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

collect_tool_mentions257–268 ↗
fn collect_tool_mentions(
    text: &str,
    mention_paths: &HashMap<String, String>,
) -> ToolMentions

作用:这个函数从用户输入文字里收集工具提及,并把已知的提及名称绑定到真实路径。它把“用户写了什么名字”和“这个名字实际指向哪里”合在一起。

数据流:进去的是一段文本,以及一个从名称到路径的映射 mention_paths;它先调用 extract_tool_mentions_from_text 把文本里出现的提及名找出来;再检查这些名字是否在 mention_paths 中有对应路径;有的话就记录到 linked_paths;最后返回 ToolMentions。

调用关系:它是技能和应用提及识别的前置步骤。测试函数也直接调用它,验证后续 find_app_mentions 能正确处理普通提及和带路径绑定的提及。

调用图:调用 1 个内部函数(extract_tool_mentions_from_text);被 2 处调用(find_app_mentions_requires_accessible_enabled_apps_for_bound_paths, find_app_mentions_requires_accessible_enabled_apps_for_slugs)。

find_skill_mentions_with_tool_mentions270–308 ↗
fn find_skill_mentions_with_tool_mentions(
    mentions: &ToolMentions,
    skills: &[SkillMetadata],
) -> Vec<SkillMetadata>

作用:这个函数根据已经解析出的提及,找出用户实际提到了哪些技能。它会同时支持按路径精确匹配和按技能名匹配。

数据流:进去的是 ToolMentions 和当前可用技能列表;它先从 linked_paths 里挑出看起来像技能路径的项,并标准化 skill:// 前缀;第一轮按技能文件路径匹配,第二轮再按技能名匹配;过程中用集合避免同一个技能重复出现;最后返回匹配到的技能列表。

调用关系:它通常接在 collect_tool_mentions 后面。路径匹配优先于名称匹配,这样当名称可能重复或改名时,带链接的提及仍能尽量指向准确技能。

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

find_app_mentions310–346 ↗
fn find_app_mentions(
    mentions: &ToolMentions,
    apps: &[AppInfo],
    skill_names_lower: &HashSet<String>,
) -> Vec<AppInfo>

作用:这个函数从用户的提及里找出被点名的应用。应用这里指可连接的外部服务,比如某个连接器。

数据流:进去的是 ToolMentions、应用列表 apps,以及已有技能名的小写集合;它先从 app:// 路径里拿到明确绑定的应用 id;再给可提及的应用计算提及短名,并只在短名唯一、没有和技能名冲突、也不是显式路径提及时才按名字选中;最后返回可访问且已启用的应用。

调用关系:它调用 app_id_from_path 解析 app:// 路径,也调用 connector_mention_slug 生成应用短名。它还依赖 is_app_mentionable 的规则,保证不可访问或被禁用的应用不会被选中。

调用图:调用 2 个内部函数(connector_mention_slug, app_id_from_path);外部调用 3 个(new, new, iter)。

is_app_mentionable348–350 ↗
fn is_app_mentionable(app: &AppInfo) -> bool

作用:这个函数判断一个应用能不能被用户提及。规则很简单:用户能访问,并且应用处于启用状态。

数据流:进去的是一个 AppInfo;它读取 is_accessible 和 is_enabled 两个布尔值;两个都为 true 就返回 true,否则返回 false。

调用关系:它被 find_app_mentions 用作门卫。无论用户是按名字提到应用,还是通过路径绑定提到应用,最终都要经过这个可用性判断。

extract_tool_mentions_from_text357–359 ↗
fn extract_tool_mentions_from_text(text: &str) -> ToolMentions

作用:这个函数从文本里提取工具提及。它使用系统默认的提及符号,比如 $ 或项目配置里的工具提及符号。

数据流:进去的是一段用户文本;它把文本和默认提及符号一起交给 extract_tool_mentions_from_text_with_sigil;出来的是 ToolMentions,里面有提到的名字和可能带上的路径。

调用关系:它是 collect_tool_mentions 的第一步。真正逐字符扫描文本的工作不在这里,而是在 extract_tool_mentions_from_text_with_sigil 里完成。

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

extract_tool_mentions_from_text_with_sigil361–418 ↗
fn extract_tool_mentions_from_text_with_sigil(text: &str, sigil: char) -> ToolMentions

作用:这个函数是真正扫描文本、找出提及的地方。sigil 是“触发符号”,也就是名字前面的特殊字符,比如 $。

数据流:进去的是文本和提及符号;它从左到右看每个字节,先尝试识别类似“$name(path)”这种带路径的链接提及,再识别普通的“$name”;它会检查名字字符是否合法,也会跳过 PATH、HOME 这类常见环境变量;最后返回收集到的 names 和 linked_paths。

调用关系:它由 extract_tool_mentions_from_text 调用。它会把链接格式交给 parse_linked_tool_mention 解析,并借助 is_common_env_var、is_mention_name_char、is_skill_path 做过滤和判断。

调用图:调用 4 个内部函数(is_common_env_var, is_mention_name_char, is_skill_path, parse_linked_tool_mention);被 1 处调用(extract_tool_mentions_from_text);外部调用 2 个(new, new)。

parse_linked_tool_mention420–475 ↗
fn parse_linked_tool_mention(
    text: &'a str,
    text_bytes: &[u8],
    start: usize,
    sigil: char,
) -> Option<(&'a str, &'a str, usize)>

作用:这个函数专门解析带链接的提及格式,例如“$tool(some/path)”。这种格式比普通名字更准确,因为它能直接带上目标路径。

数据流:进去的是原始文本、文本字节、起始位置和提及符号;它检查开头是否是左中括号加提及符号,读取合法名称,确认右中括号,再读取括号里的路径;路径不能为空;成功时返回名称、路径和解析结束位置,失败时返回 None。

调用关系:它被 extract_tool_mentions_from_text_with_sigil 在扫描到左中括号时尝试调用。它只负责一种格式,失败不会报错,外层扫描会继续按普通文本处理。

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

is_common_env_var477–493 ↗
fn is_common_env_var(name: &str) -> bool

作用:这个函数判断一个名字是不是常见环境变量。环境变量可以理解成系统里常用的配置名,比如 PATH 和 HOME。

数据流:进去的是一个名字;它先转成大写,再和一组常见环境变量名比较;如果命中就返回 true,否则返回 false。

调用关系:它被 extract_tool_mentions_from_text_with_sigil 用来减少误判。比如用户写了 $PATH,通常是在说系统路径,不是在提某个技能或工具。

调用图:被 1 处调用(extract_tool_mentions_from_text_with_sigil);外部调用 1 个(matches!)。

is_mention_name_char495–497 ↗
fn is_mention_name_char(byte: u8) -> bool

作用:这个函数判断某个字符能不能出现在提及名称里。它给提及名字定了一条简单规则。

数据流:进去的是一个字节;如果它是英文字母、数字、下划线或短横线,就返回 true;其他字符返回 false。

调用关系:它被 extract_tool_mentions_from_text_with_sigil 和 parse_linked_tool_mention 使用。没有这个检查,扫描器就不知道一个提及名应该在哪里结束。

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

is_skill_path499–501 ↗
fn is_skill_path(path: &str) -> bool

作用:这个函数判断一个路径是不是技能路径。它用排除法:不是 app、mcp、plugin 这些特殊协议,就当作技能相关路径。

数据流:进去的是一个路径字符串;它检查是否以 app://、mcp:// 或 plugin:// 开头;如果都不是,就返回 true,否则返回 false。

调用关系:它被 extract_tool_mentions_from_text_with_sigil 用来决定链接提及的名字是否也要算进普通 names。find_skill_mentions_with_tool_mentions 也依靠同样的路径语义来匹配技能。

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

normalize_skill_path503–505 ↗
fn normalize_skill_path(path: &str) -> &str

作用:这个函数把技能路径统一成便于比较的样子。它主要是去掉可选的 skill:// 前缀。

数据流:进去的是一个路径字符串;如果它以 skill:// 开头,就返回去掉这个前缀后的部分;否则原样返回。

调用关系:它在 find_skill_mentions_with_tool_mentions 中用于路径匹配前的标准化。这样同一个技能路径,不会因为有没有 skill:// 前缀而匹配失败。

app_id_from_path507–510 ↗
fn app_id_from_path(path: &str) -> Option<&str>

作用:这个函数从 app:// 路径里取出应用 id。比如 app://google_drive 会得到 google_drive。

数据流:进去的是一个路径字符串;它检查是否以 app:// 开头;如果是并且后面不是空字符串,就返回后面的 id;否则返回 None。

调用关系:它被 find_app_mentions 调用,用于处理显式绑定到应用的提及。这样用户点到的具体应用可以绕过名称猜测,直接按 id 选择。

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

tests::app517–533 ↗
fn app(id: &str, name: &str) -> AppInfo

作用:这是测试里用的小帮手,用来快速造一个 AppInfo。它让测试代码不用每次都手写一大堆应用字段。

数据流:进去的是应用 id 和名称;它填入这些值,并把测试关心的 is_accessible 和 is_enabled 默认设为 true,其他可选信息大多设为空;出来的是一个可用于测试的 AppInfo。

调用关系:它只在本文件的测试中使用。两个 find_app_mentions 测试都用它创建基础应用,再按需要改成不可访问或未启用。

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

tests::find_app_mentions_requires_accessible_enabled_apps_for_slugs536–554 ↗
fn find_app_mentions_requires_accessible_enabled_apps_for_slugs()

作用:这个测试确认:按应用短名提及时,只有“可访问且已启用”的应用会被选中。它防止禁用应用或无权限应用被误用。

数据流:进去的是测试构造的三个应用和一段包含三个提及的文本;它用 collect_tool_mentions 提取提及,再调用 find_app_mentions;最后断言结果只包含 Google Drive 这个可访问且启用的应用。

调用关系:它覆盖 find_app_mentions 的普通名字匹配路径,也间接验证 is_app_mentionable 的过滤规则确实生效。

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

tests::find_app_mentions_requires_accessible_enabled_apps_for_bound_paths557–580 ↗
fn find_app_mentions_requires_accessible_enabled_apps_for_bound_paths()

作用:这个测试确认:即使提及已经绑定了 app:// 路径,不可访问或未启用的应用也不能被选中。它防止显式路径绕过安全和开关限制。

数据流:进去的是三个测试应用、一段提及文本,以及名称到 app:// 路径的映射;它先用 collect_tool_mentions 把名字和路径合并,再调用 find_app_mentions;最后断言结果仍然只包含可访问且启用的 Google Drive。

调用关系:它覆盖 find_app_mentions 的显式路径匹配路径,并和另一个测试一起保证两种提及方式都遵守同一套可用性规则。

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

tui/src/chatwidget/ide_context.rs源码 ↗
orchestration用户输入命令时、发送聊天消息前、界面状态刷新时

这个文件解决的是一个很实际的问题:用户在终端聊天时,模型本来看不到 IDE 里正在编辑什么。打开 IDE context 后,聊天框会在每次发消息前去问 IDE:“现在用户选中了什么?打开了哪些文件?”然后把这些信息悄悄加到用户输入里,像给助手递一张当前工作台的小纸条。文件里有一个很小的状态盒子 IdeContextState,只记两件事:功能是否开启,以及这次取 IDE 信息失败时是否已经提醒过用户。ChatWidget 里的函数负责响应 /ide/ide on/ide off/ide status,更新底部状态指示,并在真正发送消息前尝试注入 IDE 上下文。一个重要细节是:如果取不到 IDE 信息,它不会每条消息都烦用户,而是只提示一次,等之后恢复可用再清掉警告状态。

函数细节9
IdeContextState::is_enabled14–16 ↗
fn is_enabled(&self) -> bool

作用:这个函数回答一个简单问题:IDE 上下文现在是不是开着。其他代码要决定是否去读取 IDE 信息时,会先问它。

数据流:进去的是当前 IdeContextState 这份状态 → 它只查看里面的 enabled 标记,不改任何东西 → 出来一个真假值,真表示开启,假表示关闭。

调用关系:它是状态盒子的查询入口。ChatWidget 在处理 /ide 命令、发送消息前、刷新底部提示时,都会靠这个结果决定下一步该做什么。

IdeContextState::enable18–21 ↗
fn enable(&mut self)

作用:这个函数把 IDE 上下文功能打开。打开时也会清掉之前的失败提醒记录,表示要重新开始尝试连接和读取。

数据流:进去的是可修改的状态盒子 → 它把 enabled 设为真,并把 prompt_fetch_warned 设为假 → 出来没有返回值,但状态已经变成“开启,且还没为失败提醒过”。

调用关系:它通常由 /ide/ide on 触发。打开后,ChatWidget 会继续检查 IDE 是否真的可用,并刷新界面上的状态提示。

IdeContextState::disable23–26 ↗
fn disable(&mut self)

作用:这个函数把 IDE 上下文功能关掉。关闭时也会清掉失败提醒记录,避免下次打开时带着旧状态。

数据流:进去的是可修改的状态盒子 → 它把 enabled 设为假,并把 prompt_fetch_warned 设为假 → 出来没有返回值,但状态已经变成“关闭”。

调用关系:它会在用户输入 /ide off 时使用,也会在尝试启用但发现 IDE 连接不可用时使用。之后 ChatWidget 会同步底部状态指示,让界面显示已关闭。

IdeContextState::mark_available28–30 ↗
fn mark_available(&mut self)

作用:这个函数表示 IDE 上下文刚刚成功取到了。它会清掉“已经提醒过失败”的标记,让以后如果又失败,可以重新提醒一次。

数据流:进去的是可修改的状态盒子 → 它把 prompt_fetch_warned 设为假 → 出来没有返回值,但失败提醒状态被重置了。

调用关系:当 ChatWidget 成功从 IDE 拿到上下文时会用它。它的作用像是告诉系统:“刚才连上了,之前的失败记录不算了。”

ChatWidget::handle_ide_command34–43 ↗
fn handle_ide_command(&mut self)

作用:这个函数处理用户直接输入 /ide 的情况。它像一个开关:当前开着就关掉,当前关着就尝试打开并显示状态。

数据流:进去的是聊天界面当前状态 → 它先查看 IDE 上下文是否开启;如果已开启,就关闭状态、刷新底部指示、显示“IDE context is off.”;如果未开启,就打开状态,再去显示启用结果 → 出来没有返回值,但界面状态和提示消息会被更新。

调用关系:它是无参数 /ide 命令的实际执行者。ChatWidget::handle_ide_command_args 在发现用户没有带参数时会把工作交给它;它自己会调用 ChatWidget::sync_ide_context_status_indicatorChatWidget::add_ide_context_status_message 来完成界面反馈。

调用图:调用 2 个内部函数(add_ide_context_status_message, sync_ide_context_status_indicator);被 1 处调用(handle_ide_command_args)。

ChatWidget::handle_ide_command_args45–64 ↗
fn handle_ide_command_args(&mut self, args: &str)

作用:这个函数处理带参数的 /ide 命令,比如 /ide on/ide off/ide status。它负责把用户写的文字翻译成具体动作。

数据流:进去的是用户在 /ide 后面输入的参数字符串 → 它先转成小写来避免大小写差异;空参数就切换开关,on 就开启并检查状态,off 就关闭并提示,status 就只显示当前状态,其他内容就显示用法错误 → 出来没有返回值,但可能改变 IDE 上下文开关、刷新底部指示、增加提示或错误消息。

调用关系:它是 /ide ... 命令的分发点。遇到空参数时交给 ChatWidget::handle_ide_command,遇到开启或查看状态时会调用 ChatWidget::add_ide_context_status_message,关闭时会调用 ChatWidget::sync_ide_context_status_indicator

调用图:调用 3 个内部函数(add_ide_context_status_message, handle_ide_command, sync_ide_context_status_indicator)。

ChatWidget::maybe_apply_ide_context67–89 ↗
fn maybe_apply_ide_context(&mut self, items: &mut Vec<UserInput>)

作用:这个函数在用户消息真正发出去前运行。它会在 IDE 上下文开启时,尝试把当前 IDE 信息加入这条消息,让助手更懂用户正在看的代码。

数据流:进去的是聊天界面状态,以及即将发送的一组用户输入 items → 如果 IDE 上下文没开启,它什么都不做;如果开启,就用当前工作目录去读取 IDE 上下文。读取成功时,它标记 IDE 可用、刷新底部指示,并把上下文合并进 items;读取失败时,它刷新指示,并且如果之前没提醒过,就显示一次“这条消息跳过了 IDE context”的提示 → 出来没有普通返回值,但 items 可能被加入额外上下文,界面提示和内部警告标记也可能改变。

调用关系:它位于“发送用户消息”这条路上,是把 IDE 信息真正放进提示词的地方。它会调用外部的 fetch_ide_context 去拿 IDE 信息,成功后交给外部的 apply_ide_context_to_user_input 合并到用户输入里,并用 ChatWidget::sync_ide_context_status_indicator 更新界面。

调用图:调用 1 个内部函数(sync_ide_context_status_indicator);外部调用 2 个(apply_ide_context_to_user_input, fetch_ide_context)。

ChatWidget::add_ide_context_status_message91–126 ↗
fn add_ide_context_status_message(&mut self)

作用:这个函数给用户显示 IDE 上下文的当前状态。它不只是说开或关,还会实际试一下能不能连到 IDE,从而告诉用户这个功能是否真的可用。

数据流:进去的是聊天界面当前状态 → 如果功能没开启,它刷新底部指示并显示关闭;如果已开启,它尝试读取 IDE 上下文。读取成功时,它标记可用、刷新指示,并根据是否有可注入的提示上下文显示不同说明;读取失败时,它关闭该功能、刷新指示,并显示无法启用以及给用户看的解决建议 → 出来没有返回值,但可能改变开关状态、清理警告标记、刷新底部指示并添加提示消息。

调用关系:它是 /ide on/ide status/ide 开启分支背后的状态检查员。ChatWidget::handle_ide_commandChatWidget::handle_ide_command_args 会调用它;它自己会调用外部的 fetch_ide_contexthas_prompt_context 来判断 IDE 是否连得上、有没有能加入提示词的内容。

调用图:调用 1 个内部函数(sync_ide_context_status_indicator);被 2 处调用(handle_ide_command, handle_ide_command_args);外部调用 2 个(fetch_ide_context, has_prompt_context)。

ChatWidget::sync_ide_context_status_indicator128–131 ↗
fn sync_ide_context_status_indicator(&mut self)

作用:这个函数把内部的 IDE 上下文开关同步到底部界面的状态灯上。它保证用户看到的开关状态和程序心里的状态一致。

数据流:进去的是聊天界面当前状态 → 它读取 IDE 上下文是否开启 → 把这个真假值交给底部面板,让底部面板显示“IDE context 是否活跃” → 出来没有返回值,但界面上的状态指示会更新。

调用关系:它是所有状态变化后的收尾动作。ChatWidget::handle_ide_commandChatWidget::handle_ide_command_argsChatWidget::maybe_apply_ide_contextChatWidget::add_ide_context_status_message 都会在需要刷新用户可见状态时调用它。

调用图:被 4 处调用(add_ide_context_status_message, handle_ide_command, handle_ide_command_args, maybe_apply_ide_context)。

tui/src/chatwidget/goal_menu.rs源码 ↗
orchestrationuser command handling

这个文件像是聊天窗口里“目标功能”的小控制面板。用户输入 /goal 时,它负责把目标的摘要用几行清楚的话显示出来,比如目标内容、状态、用了多久、用了多少 token(token 可以理解成模型处理文字的计量单位)。用户要改目标时,它会弹出一个输入框;用户要恢复暂停的目标时,它会弹出一个二选一菜单。它还会在目标被清掉后,同步更新当前聊天窗口的状态提示,避免界面还显示旧目标。这里的重点不是保存目标本身,而是把目标信息翻译成用户能看懂、能点击或输入的界面,并把用户选择转成 AppEvent(应用内部事件,像给主程序递一张办事单)发出去。

函数细节7
ChatWidget::show_goal_summary9–11 ↗
fn show_goal_summary(&mut self, goal: AppThreadGoal)

作用:把一个目标的当前情况显示到聊天历史里。用户执行裸 /goal 命令时,就能看到目标概览,而不是只得到一堆内部数据。

数据流:进去的是一个 AppThreadGoal,也就是当前对话线程的目标信息。它把这个目标交给 goal_summary_lines 变成一组适合界面显示的文字行,然后把这些行追加到聊天历史里。出来的结果不是返回值,而是聊天窗口多了一段目标摘要。

调用关系:它是目标摘要显示流程的入口。自己不负责拼每一行文字,而是把排版工作交给 goal_summary_lines,拿到结果后再放进 ChatWidget 的历史区域。

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

ChatWidget::show_goal_edit_prompt13–37 ↗
fn show_goal_edit_prompt(&mut self, thread_id: ThreadId, goal: AppThreadGoal)

作用:弹出一个“编辑目标”的输入框,让用户修改目标内容。它还会保留或调整目标状态,保证编辑完以后目标处在合理状态。

数据流:进去的是线程编号 thread_id 和当前目标 goal。函数先拿到应用事件发送器,再根据原状态算出编辑后的状态,并记住原来的 token 预算。然后它创建一个 CustomPromptView,也就是一个自定义输入框;用户输入新目标并按 Enter 后,闭包会发送 SetThreadGoalDraft 事件。出来的结果是底部面板显示输入框,并且用户提交时会把新草稿发给应用主流程。

调用关系:它在用户选择编辑目标时被调用。它会调用 edited_goal_status 来判断编辑后状态该怎么处理,也会创建 CustomPromptView。真正保存或应用目标修改的工作不在这里做,而是通过 AppEvent 交给应用的事件处理流程。

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

ChatWidget::show_resume_paused_goal_prompt39–72 ↗
fn show_resume_paused_goal_prompt(
        &mut self,
        thread_id: ThreadId,
        objective: String,
    )

作用:在用户想恢复暂停目标时,弹出确认菜单。这样可以避免用户误操作,也让用户清楚知道恢复后目标会继续运行。

数据流:进去的是线程编号 thread_id 和目标文字 objective。函数准备一个“恢复目标”的动作:如果用户选中它,就发送 SetThreadGoalStatus 事件,把状态改成 Active。然后它创建一个选择菜单,里面有“恢复目标”和“保持暂停”两个选项。出来的结果是界面上出现这个选择弹窗;如果用户选恢复,就会发出恢复事件。

调用关系:它是恢复暂停目标前的确认环节。它自己不直接改目标,而是在菜单动作里把请求发给应用事件系统;菜单的展示则交给 show_selection_view 完成。

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

ChatWidget::on_thread_goal_cleared74–82 ↗
fn on_thread_goal_cleared(&mut self, thread_id: &str)

作用:当某个线程的目标被清除后,检查是不是当前正在看的线程;如果是,就把界面上的目标状态也清掉。这样不会出现目标已经没了、界面还显示它存在的尴尬情况。

数据流:进去的是被清除目标所属的 thread_id 字符串。函数把它和当前 ChatWidget 正在显示的线程编号比较;如果一致,就把 current_goal_status 设为 None,并刷新协作模式指示器。出来的结果是当前界面的目标状态提示被同步清空。

调用关系:它通常由外部目标清除事件触发。它不负责清除底层目标文件或数据,只负责让当前聊天窗口的显示状态跟真实状态保持一致。

goal_summary_lines85–120 ↗
fn goal_summary_lines(goal: &AppThreadGoal) -> Vec<Line<'static>>

作用:把目标对象整理成几行适合显示在终端界面里的文字。它解决的是“内部目标数据怎么变成用户能一眼看懂的摘要”的问题。

数据流:进去的是目标 AppThreadGoal。函数读取目标状态、目标内容、已用时间、已用 token,以及可选的 token 预算;它把时间和 token 转成人类更容易读的格式,再根据目标状态补上一行可用命令提示。出来的是 Vec<Line<'static>>,也就是一组界面文字行,供聊天历史显示。

调用关系:它是 show_goal_summary 的排版帮手。show_goal_summary 负责把内容放到界面里,它负责把目标内容先加工成漂亮、清楚的文字行。

调用图:被 1 处调用(show_goal_summary);外部调用 3 个(default, from, vec!)。

goal_status_label122–131 ↗
fn goal_status_label(status: AppThreadGoalStatus) -> &'static str

作用:把程序内部的目标状态转换成普通英文短语,比如 active、paused、complete。这样界面不会直接暴露难懂的枚举名字。

数据流:进去的是 AppThreadGoalStatus,也就是目标当前状态。函数根据不同状态挑一个固定文本。出来的是一个静态字符串,用来显示给用户看。

调用关系:它是目标摘要文字的一块小翻译表。生成目标摘要时需要它把内部状态翻成界面标签,避免每个显示位置都重复写同样的状态判断。

edited_goal_status133–143 ↗
fn edited_goal_status(status: AppThreadGoalStatus) -> AppThreadGoalStatus

作用:决定用户编辑目标后,这个目标应该是什么状态。它避免已经完成或被预算限制的目标在编辑后还停在不适合继续运行的状态。

数据流:进去的是编辑前的目标状态。函数按规则处理:原本 active 就继续 active;paused、blocked、usage limited 保持原样;budget limited 或 complete 则改回 active。出来的是编辑后的目标状态。

调用关系:它被 show_goal_edit_prompt 调用。编辑目标弹窗在发送更新事件前,会先用它算好新状态,再把状态和 token 预算一起放进 AppEvent 交给后续流程处理。

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

tui/src/chatwidget/hooks.rs源码 ↗
orchestration用户打开 hooks 列表时、后台 hooks 列表加载完成时

这里的“hook”可以理解成项目里预先设置好的小机关:某些时机到了,就自动执行一些脚本或动作。这个文件不自己去磁盘里找 hook,而是像前台接待一样:用户想看 hook 时,它先发一个事件,请后台去取当前目录的 hook 列表;后台取完后,它检查结果是不是还属于当前目录,避免用户切换目录后显示旧内容;如果成功,就把数据整理成底部面板能看的形式并打开浏览器视图;如果失败,就在聊天界面里显示错误。它的重要点是防止“拿到过期结果还照样显示”,也让界面刷新和错误提示都走统一流程。

函数细节3
ChatWidget::add_hooks_output11–15 ↗
fn add_hooks_output(&mut self)

作用:这个函数在用户想查看 hooks 列表时使用。它不直接读取 hooks,而是发出一个“请帮我读取当前目录 hooks”的应用事件。

数据流:进去的是当前聊天界面的状态,尤其是配置里的当前工作目录。它把这个目录复制成一个路径,包装成 FetchHooksList 事件,发给应用的事件通道。出来的不是直接结果,而是一个已经发出去的请求;真正的列表会稍后异步回来。

调用关系:它是整个 hooks 查看流程的起点。用户触发查看动作后,它把活儿交给应用事件系统,后续后台读取完成后,会再回到 ChatWidget::on_hooks_loaded 处理结果。

ChatWidget::on_hooks_loaded17–32 ↗
fn on_hooks_loaded(
        &mut self,
        cwd: PathBuf,
        result: Result<HooksListResponse, String>,
    )

作用:这个函数处理 hooks 列表读取完成后的结果。它会确认结果属于当前目录,成功就打开 hooks 浏览器,失败就显示错误。

数据流:进去的是一个目录路径和一次读取结果:结果可能是 hooks 列表,也可能是一段错误文字。它先把传入目录和当前配置里的目录比较;如果不一样,说明这是旧请求的结果,直接丢掉。目录一致时,如果读取成功,就用 hooks_list_entry_for_cwd 把响应整理成当前目录对应的入口,再交给 open_hooks_browser 显示;如果读取失败,就把错误格式化成“Failed to load hooks: ...”并加到界面消息里。

调用关系:它位于后台读取完成之后、界面展示之前。它会调用 hooks_list_entry_for_cwd 做结果整理,也会调用 ChatWidget::open_hooks_browser 真正打开底部视图。这样可以把“检查结果是否过期”和“显示界面”分开处理。

调用图:调用 2 个内部函数(open_hooks_browser, hooks_list_entry_for_cwd);外部调用 2 个(as_path, format!)。

ChatWidget::open_hooks_browser34–42 ↗
fn open_hooks_browser(&mut self, entry: HooksListEntry)

作用:这个函数真正把 hooks 列表浏览器显示到底部面板里。它让用户能在聊天界面下方看到并操作 hooks 条目。

数据流:进去的是一个 HooksListEntry,也就是已经整理好的 hooks 列表入口。它用这个入口、应用事件发送器的副本,以及底部面板的快捷键设置,创建一个 HooksBrowserView;然后把这个视图放进 bottom_pane 显示。最后它请求界面重绘,让新面板马上出现在屏幕上。

调用关系:它是展示阶段的最后一步,通常由 ChatWidget::on_hooks_loaded 在读取成功后调用。它把数据交给 HooksBrowserView::from_entry 变成可显示的视图,再交给 bottom_pane.show_view 接管显示。

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

tui/src/chatwidget/slash_dispatch.rs源码 ↗
orchestrationrequest handling / main loop

用户在聊天框里输入普通文字时,会发给模型;但输入 / 开头的内容时,很多时候不是聊天,而是在控制应用本身。这个文件就是 ChatWidget 的“斜杠命令调度台”。它先检查命令现在能不能用,比如任务正在跑、侧边对话里、代码审查中,有些命令就要拦住并提示原因。然后它根据命令种类做不同动作:打开菜单、发应用事件、提交一段预置提示、切换模式、启动侧边对话、显示 diff、设置目标等。它还特别照顾“带参数的命令”和“排队后再执行的命令”,比如 /goal xxx 或会话还没准备好时先存起来的输入。另一个重要点是历史记录:用户按上箭头回看输入时,斜杠命令要像普通发送内容一样出现一次,不能漏记,也不能重复记。

函数细节20
ChatWidget::handle_slash_command_dispatch47–53 ↗
fn handle_slash_command_dispatch(&mut self, cmd: SlashCommand)

作用:处理一个不带参数的斜杠命令,并把这次命令记进本地输入历史。这样用户之后按上箭头时,能找回刚才执行过的命令。

数据流:进去的是一个已经识别好的 SlashCommand。它先交给 dispatch_command 真正执行;如果是 /goal,还会清掉输入框里临时保存的提交状态;最后把待记录的斜杠命令写入历史。出来没有返回值,但界面状态、事件队列或历史记录可能已经改变。

调用关系:这是普通斜杠命令从输入框进入应用层的入口。它自己不判断每个命令该干什么,而是先交给 ChatWidget::dispatch_command,等命令处理完再补上历史记录这件事。

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

ChatWidget::handle_service_tier_command_dispatch55–67 ↗
fn handle_service_tier_command_dispatch(&mut self, command: ServiceTierCommand)

作用:处理切换服务档位的命令,也就是用户通过斜杠命令改变当前模型服务等级。它会防止这类命令在侧边对话里误用。

数据流:进去的是一个 ServiceTierCommand,里面带着命令名和要切换到的档位信息。函数先看当前是否在侧边对话;如果是,就显示错误、清理临时提交状态并记录历史后结束。如果不是,就调用界面切换服务档位的逻辑,再记录历史。结果是服务档位可能被改变,或者用户看到一条不能使用的提示。

调用关系:排队的斜杠输入被重新取出来执行时,ChatWidget::submit_queued_slash_prompt 可能会调用它。它承担的是服务档位命令的特殊分支,不走普通内置命令的 dispatch_command。

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

ChatWidget::handle_slash_command_with_args_dispatch74–82 ↗
fn handle_slash_command_with_args_dispatch(
        &mut self,
        cmd: SlashCommand,
        args: String,
        text_elements: Vec<TextElement>,
    )

作用:处理带参数的斜杠命令,比如 /rename 新名字 或 /goal 做某件事。它还负责保证这种命令在输入历史里只记一次。

数据流:进去的是命令本身、参数文字,以及参数对应的富文本元素。它把这些交给 dispatch_command_with_args 去执行,然后记录待保存的命令历史。函数不返回结果,但可能触发提交消息、打开弹窗、发应用事件或改动输入历史。

调用关系:这是“用户刚刚现场输入了带参数命令”时的入口。实际怎么解释参数、是否要转成普通消息,都由 ChatWidget::dispatch_command_with_args 继续处理。

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

ChatWidget::apply_plan_slash_command84–102 ↗
fn apply_plan_slash_command(&mut self) -> bool

作用:尝试把当前聊天切到“计划模式”。计划模式可以理解为先让系统规划怎么做,而不是立刻动手执行。

数据流:它读取当前配置和模型目录,先确认协作模式是否开启;没开启就显示提示并返回 false。开启后,它通过 plan_mask 找到计划模式需要的设置;找到了就应用这个设置并返回 true,找不到就提示暂时不可用并返回 false。

调用关系:ChatWidget::dispatch_command 处理单独的 /plan 时会用它;ChatWidget::dispatch_prepared_command_with_args 处理 /plan 后面带文字时也会先用它。它是进入计划模式前的门禁。

调用图:调用 1 个内部函数(plan_mask);被 2 处调用(dispatch_command, dispatch_prepared_command_with_args)。

ChatWidget::request_side_conversation104–115 ↗
fn request_side_conversation(
        &mut self,
        parent_thread_id: ThreadId,
        user_message: Option<UserMessage>,
    )

作用:请求开启一个侧边对话。侧边对话可以理解为从当前主会话旁边开一个分支,用来单独讨论一件事。

数据流:进去的是父会话的线程 ID,以及可选的一条用户消息。它先把界面上的侧边上下文标签设成“正在启动”,要求界面重画,然后发送 StartSide 事件给应用主流程。结果是界面开始显示启动状态,后台收到开侧边对话的请求。

调用关系:ChatWidget::request_empty_side_conversation 会用它启动空侧边对话;ChatWidget::dispatch_prepared_command_with_args 在处理带内容的 /side 或 /btw 时也会用它,并把那段内容作为侧边对话的第一条消息。

调用图:被 2 处调用(dispatch_prepared_command_with_args, request_empty_side_conversation)。

ChatWidget::request_empty_side_conversation117–127 ↗
fn request_empty_side_conversation(&mut self, cmd: SlashCommand)

作用:处理不带内容的 /side 或 /btw,请求开一个空的侧边对话。如果主会话还没真正开始,它会拦住,因为没有父会话可挂靠。

数据流:进去的是触发它的 SlashCommand。它读取当前 thread_id;如果没有线程 ID,就显示“会话开始前不能用”的错误。如果有,就把线程 ID 和空消息交给 request_side_conversation。结果是侧边对话被请求启动,或者用户看到错误提示。

调用关系:ChatWidget::dispatch_command 在遇到单独的 /side 或 /btw 时调用它。它再把真正发送启动事件的工作交给 ChatWidget::request_side_conversation。

调用图:调用 2 个内部函数(request_side_conversation, command);被 1 处调用(dispatch_command);外部调用 1 个(format!)。

ChatWidget::emit_raw_output_mode_changed129–132 ↗
fn emit_raw_output_mode_changed(&self, enabled: bool)

作用:通知应用其它部分:原始输出模式已经开了或关了。原始输出模式大致就是更少加工地显示模型或系统输出。

数据流:进去的是一个布尔值 enabled,表示是否开启。它把这个值包装成 RawOutputModeChanged 事件,通过 app_event_tx 发出去。函数没有返回值,但应用事件队列多了一条状态变化通知。

调用关系:ChatWidget::dispatch_command 处理单独 /raw 切换时会调用它;ChatWidget::dispatch_prepared_command_with_args 处理 /raw on 或 /raw off 时也会调用它。它只负责广播变化,不负责决定开关。

调用图:被 2 处调用(dispatch_command, dispatch_prepared_command_with_args)。

ChatWidget::dispatch_command134–525 ↗
fn dispatch_command(&mut self, cmd: SlashCommand)

作用:这是无参数斜杠命令的总调度函数。它决定 /new、/diff、/model、/quit 等每个命令具体要触发什么动作。

数据流:进去的是一个 SlashCommand。它先做通用检查:侧边对话里能不能用、代码审查时能不能用、任务运行中能不能用。通过检查后,它按命令分支执行:有的发送 AppEvent,有的打开选择菜单,有的改界面状态,有的提交预置消息,有的异步计算 git diff。没有统一返回值,但它会改变界面、发送事件、记录提示或启动后台任务。

调用关系:ChatWidget::handle_slash_command_dispatch 会把普通命令交给它;ChatWidget::dispatch_command_with_args 和 ChatWidget::dispatch_prepared_command_with_args 在发现参数为空或不适合处理时也会回退到它;ChatWidget::submit_queued_slash_prompt 处理排队命令时也可能直接调用它。它是这个文件的核心路由器。

调用图:调用 8 个内部函数(apply_plan_slash_command, emit_raw_output_mode_changed, ensure_side_command_allowed_outside_review, ensure_slash_command_allowed_in_side_conversation, ensure_usage_command_available, request_empty_side_conversation, available_during_task, level_from_config);被 4 处调用(dispatch_command_with_args, dispatch_prepared_command_with_args, handle_slash_command_dispatch, submit_queued_slash_prompt);外部调用 12 个(default, from, from, DiffResult, feedback_disabled_params, feedback_selection_params, format!, new_error_event, include_str!, matches! (+2 more))。

ChatWidget::dispatch_command_with_args532–597 ↗
fn dispatch_command_with_args(
        &mut self,
        cmd: SlashCommand,
        args: String,
        text_elements: Vec<TextElement>,
    )

作用:这是现场输入的“带参数斜杠命令”的第一层处理。它判断这个命令到底支不支持参数,并把参数整理到后续流程。

数据流:进去的是命令、参数字符串和文本元素。它先做和普通命令一样的可用性检查;如果命令不支持参数,就退回 dispatch_command;如果任务正在跑且命令不允许,就显示错误。参数为空时也退回普通命令。对于 /goal,它保留粘贴、图片等输入附件;其它命令会先用 prepare_live_inline_args 整理当前输入,再交给 dispatch_prepared_command_with_args。结果是命令被执行、被拒绝,或参数被准备好继续处理。

调用关系:ChatWidget::handle_slash_command_with_args_dispatch 调用它。它是“刚从输入框提交出来”的带参数命令和真正执行函数 ChatWidget::dispatch_prepared_command_with_args 之间的转换层。

调用图:调用 7 个内部函数(dispatch_command, dispatch_prepared_command_with_args, ensure_side_command_allowed_outside_review, ensure_slash_command_allowed_in_side_conversation, prepare_live_inline_args, available_during_task, supports_inline_args);被 1 处调用(handle_slash_command_with_args_dispatch);外部调用 3 个(new, format!, new_error_event)。

ChatWidget::prepare_live_inline_args599–610 ↗
fn prepare_live_inline_args(
        &mut self,
        args: String,
        text_elements: Vec<TextElement>,
    ) -> Option<(String, Vec<TextElement>)>

作用:整理用户现场输入的命令参数,确保拿到的是应该提交的那段文字。它还避免把同一个命令重复写进历史。

数据流:进去的是参数字符串和文本元素。它检查输入框当前文字是否为空;如果为空,说明传进来的参数已经够用,就原样返回。如果不为空,就请底部输入区准备一次“内联参数提交”,并明确不要走普通历史记录。返回值要么是整理后的参数和文本元素,要么是 None 表示准备失败或不应继续。

调用关系:ChatWidget::dispatch_command_with_args 在处理大多数现场带参命令时调用它。它依赖 bottom_pane 做输入框层面的细节整理。

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

ChatWidget::clear_live_goal_submission612–617 ↗
fn clear_live_goal_submission(&mut self)

作用:清掉现场输入 /goal 时留下的输入框内容和临时提交状态。这样目标命令被处理后,输入框不会残留旧内容。

数据流:它不接收外部参数,只操作当前 ChatWidget 的底部输入区。它把输入框文字、文本元素、待粘贴内容都清空,并清理待提交状态。结果是底部输入区回到干净状态。

调用关系:ChatWidget::dispatch_prepared_command_with_args 在处理 /goal 的多个分支后会调用它,尤其是现场输入而不是排队输入时。它是 /goal 特殊收尾动作的一部分。

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

ChatWidget::prepared_inline_user_message619–642 ↗
fn prepared_inline_user_message(
        &mut self,
        args: String,
        text_elements: Vec<TextElement>,
        mut local_images: Vec<LocalImageAttachment>,
        mut remote_image_urls: V

作用:把已经整理好的斜杠命令参数变成一条真正的用户消息。带图片、远程图片链接和提及绑定时,也会一起装进去。

数据流:进去的是文字、文本元素、图片、远程图片地址、提及绑定,以及来源是现场输入还是排队输入。若来源是现场输入,它会从底部输入区取走最近提交的图片、远程图片地址和提及绑定,覆盖传入的对应数据;若是排队输入,就使用传入数据。最后产出一个 UserMessage。

调用关系:ChatWidget::dispatch_prepared_command_with_args 在处理 /plan 带内容、/side 带内容等需要把参数当成用户消息发送的命令时调用它。它负责把零散输入材料打包成标准消息。

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

ChatWidget::dispatch_prepared_command_with_args644–888 ↗
fn dispatch_prepared_command_with_args(
        &mut self,
        cmd: SlashCommand,
        prepared: PreparedSlashCommandArgs,
    )

作用:执行已经准备好的带参数斜杠命令。它处理真正需要看参数内容的命令,比如 /usage daily、/raw on、/rename 名字、/goal 目标、/side 内容。

数据流:进去的是命令和 PreparedSlashCommandArgs,里面包含参数文字、文本元素、粘贴内容、图片、提及绑定和来源。它先取出并修剪参数,然后按命令逐个解释:有的校验固定写法,有的打开调试界面,有的设置原始输出模式,有的改线程名,有的把参数当用户消息提交,有的设置或清除目标,有的启动侧边对话。最后,如果这是现场输入且不是 /goal,它会清理待提交状态。

调用关系:ChatWidget::dispatch_command_with_args 会把现场带参命令交给它;ChatWidget::submit_queued_slash_prompt 会把排队后重新解析出的带参命令交给它。它内部会调用 apply_plan_slash_command、prepared_inline_user_message、request_side_conversation、emit_raw_output_mode_changed 等小部件完成具体动作,必要时也会退回 dispatch_command。

调用图:调用 10 个内部函数(apply_plan_slash_command, clear_live_goal_submission, dispatch_command, emit_raw_output_mode_changed, ensure_usage_command_available, prepared_inline_user_message, request_side_conversation, parse, from_config, command);被 2 处调用(dispatch_command_with_args, submit_queued_slash_prompt);外部调用 7 个(SetStatus, from, new, review, ResumeSessionByIdOrName, format!, matches!)。

ChatWidget::submit_queued_slash_prompt890–996 ↗
fn submit_queued_slash_prompt(
        &mut self,
        queued_message: QueuedUserMessage,
    ) -> QueueDrain

作用:处理之前排队保存下来的用户输入,并判断它是不是斜杠命令。常见场景是会话还没准备好,用户先输入了内容,等系统准备好后再拿出来执行。

数据流:进去的是 QueuedUserMessage,里面有用户消息和待粘贴内容。它先从文本里解析 /命令名;解析不到,或命令名里含有斜杠,就当普通用户消息提交。如果识别到命令,就根据当前可用命令列表查找;找不到就提示不认识并继续处理队列。没有参数的命令会直接执行;有参数且支持内联参数的内置命令,会裁出参数对应的文本元素,再交给 dispatch_prepared_command_with_args。最后返回 QueueDrain,告诉队列是继续处理下一条还是先停下。

调用关系:这是排队输入回放时的关键入口。它会调用 parse_slash_name 和 find_slash_command 来识别命令,调用 builtin_command_flags 决定哪些命令当前可见,再按情况交给 dispatch_command、dispatch_prepared_command_with_args 或 handle_service_tier_command_dispatch。

调用图:调用 7 个内部函数(parse_slash_name, find_slash_command, builtin_command_flags, dispatch_command, dispatch_prepared_command_with_args, handle_service_tier_command_dispatch, queued_command_drain_result);外部调用 2 个(slash_command_args_elements, format!)。

ChatWidget::builtin_command_flags998–1018 ↗
fn builtin_command_flags(&self) -> BuiltinCommandFlags

作用:生成一组“当前哪些内置命令应该可用”的开关。它让命令查找逻辑能按配置、登录状态、系统平台和当前会话状态过滤命令。

数据流:它读取当前配置、功能开关、登录状态、是否启用协作模式、是否在侧边对话、是否 Windows 特定沙箱模式等信息。然后组装成 BuiltinCommandFlags 返回。结果是一份给命令查找器使用的能力清单。

调用关系:ChatWidget::submit_queued_slash_prompt 在重新识别排队斜杠命令时调用它。它不执行命令,只告诉 find_slash_command 当前应该承认哪些命令。

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

ChatWidget::ensure_usage_command_available1020–1026 ↗
fn ensure_usage_command_available(&mut self) -> bool

作用:检查 /usage 命令现在能不能用。这个命令需要 ChatGPT 登录状态,否则没有用量数据可看。

数据流:它读取 has_codex_backend_auth。若已经登录可用,就返回 true;否则显示“需要用 ChatGPT 登录”的错误,并返回 false。它不改变其它业务状态。

调用关系:ChatWidget::dispatch_command 处理单独 /usage 时会先问它;ChatWidget::dispatch_prepared_command_with_args 处理 /usage daily 等带参数写法时也会先问它。它是 /usage 的权限门卫。

调用图:被 2 处调用(dispatch_command, dispatch_prepared_command_with_args)。

ChatWidget::queued_command_drain_result1028–1089 ↗
fn queued_command_drain_result(&self, cmd: SlashCommand) -> QueueDrain

作用:决定执行完一个排队斜杠命令后,队列要不要继续往下处理。它避免在界面弹窗、任务运行或需要用户操作时继续自动执行后面的输入。

数据流:进去的是刚执行过的 SlashCommand。它先看是否有用户回合正在等待或运行,或者当前是否有弹窗;如果有,就返回 Stop。否则它按命令类型判断:像 /status、/diff、/raw 这类不会阻塞用户选择的命令可以 Continue;像 /model、/permissions、/quit、/goal 等可能打开界面或改变流程的命令会 Stop。

调用关系:ChatWidget::submit_queued_slash_prompt 在执行排队命令后调用它。它给队列处理器一个简单答案:继续读下一条,还是先停下来等当前动作稳定。

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

ChatWidget::slash_command_args_elements1091–1114 ↗
fn slash_command_args_elements(
        rest: &str,
        rest_offset: usize,
        text_elements: &[TextElement],
    ) -> Vec<TextElement>

作用:从整条输入的文本标记里,裁出只属于命令参数的那一部分。文本标记可以理解为文字里的特殊片段信息,比如提及、格式范围等。

数据流:进去的是参数文本 rest、参数在原始整条输入里的起始字节位置 rest_offset,以及整条输入的 TextElement 列表。它过滤掉完全在参数前面的元素,把落在参数里的元素位置整体平移,让它们从参数字符串的 0 开始计算,并裁掉越界部分。返回的是新的 TextElement 列表。

调用关系:ChatWidget::submit_queued_slash_prompt 在处理排队的带参斜杠命令时使用它。因为排队消息保存的是整条文本的位置,它需要这个函数把位置换算成“只看参数”的位置。

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

ChatWidget::ensure_slash_command_allowed_in_side_conversation1116–1126 ↗
fn ensure_slash_command_allowed_in_side_conversation(&mut self, cmd: SlashCommand) -> bool

作用:检查某个斜杠命令在侧边对话里能不能用。侧边对话环境比较特殊,有些控制主会话的命令不应该在那里执行。

数据流:进去的是 SlashCommand。它读取 active_side_conversation;如果当前不在侧边对话,或者这个命令声明允许在侧边对话使用,就返回 true。否则显示错误提示、清理待提交状态,并返回 false。

调用关系:ChatWidget::dispatch_command 和 ChatWidget::dispatch_command_with_args 在真正执行命令前都会调用它。它是所有斜杠命令进入侧边对话环境时的第一道安全检查。

调用图:调用 1 个内部函数(available_in_side_conversation);被 2 处调用(dispatch_command, dispatch_command_with_args);外部调用 1 个(format!)。

ChatWidget::ensure_side_command_allowed_outside_review1128–1139 ↗
fn ensure_side_command_allowed_outside_review(&mut self, cmd: SlashCommand) -> bool

作用:防止在代码审查运行时启动 /side 或 /btw。代码审查期间开侧边对话可能打乱当前审查流程,所以这里直接拦住。

数据流:进去的是 SlashCommand。它只关心 /side 和 /btw,并检查 review.is_review_mode。如果不是这两个命令,或者当前不在审查模式,就返回 true。否则显示“代码审查运行时不可用”的错误,清理待提交状态,并返回 false。

调用关系:ChatWidget::dispatch_command 和 ChatWidget::dispatch_command_with_args 都会在执行前调用它。它和 ensure_slash_command_allowed_in_side_conversation 一起组成斜杠命令的通用门禁。

调用图:调用 1 个内部函数(command);被 2 处调用(dispatch_command, dispatch_command_with_args);外部调用 2 个(format!, matches!)。

协议驱动更新

将后端通知和请求映射为具体的聊天控件生命周期、审批和关闭 UI 行为。

tui/src/chatwidget/protocol.rs源码 ↗
orchestration收到服务器通知时

聊天窗口不是自己凭空变化的,它要听服务器不断发来的通知。这个文件就像前台接线员:先判断消息是不是发给当前会话的,避免别的子会话消息误改这里;再区分这是实时消息,还是从历史记录里重放出来的消息。然后它按通知类型分流:任务开始就显示忙碌状态,任务结束就收尾,工具开始就加一块进度卡片,文字增量就追加到回答里,错误就显示给用户。它还特别处理“会重试的错误”和“不可重试的错误”,避免同一个错误重复吓用户。没有它,服务器说了什么,聊天界面就不知道该怎么变,用户看到的状态会乱套。

函数细节4
ChatWidget::handle_server_notification4–227 ↗
fn handle_server_notification(
        &mut self,
        notification: ServerNotification,
        replay_kind: Option<ReplayKind>,
    )

作用:这是服务器通知进入聊天窗口后的总入口。它看通知是什么类型,再把它交给对应的小处理器,让界面做出正确反应。

数据流:进去的是一个服务器通知,以及一个可选的重放标记;函数先检查某些带会话编号的通知是不是送错地方,再判断它是不是历史重放、是不是会自动重试的错误;之后按通知种类更新聊天窗口状态、追加文字、显示警告、刷新工具状态或结束任务。出来没有单独返回值,但聊天窗口内部状态和屏幕内容会被改动。

调用关系:它是这个文件里的调度中心。遇到会话名更新时会用 from_string 把字符串形式的会话编号转成程序认识的编号,失败时用 warn! 记一条警告;遇到任务结束、条目开始、条目完成时,分别把活交给 ChatWidget::handle_turn_completed_notification、ChatWidget::handle_item_started_notification 和 ChatWidget::handle_item_completed_notification。matches! 用来做简单判断,比如确认某个通知是不是特定状态。

调用图:调用 4 个内部函数(from_string, handle_item_completed_notification, handle_item_started_notification, handle_turn_completed_notification);外部调用 2 个(matches!, warn!)。

ChatWidget::handle_turn_completed_notification229–277 ↗
fn handle_turn_completed_notification(
        &mut self,
        notification: TurnCompletedNotification,
        replay_kind: Option<ReplayKind>,
    )

作用:这个函数专门处理“一轮对话结束了”这件事。它根据这一轮是成功、被打断、失败,决定聊天窗口该收尾、显示中断,还是展示错误。

数据流:进去的是一条任务结束通知,以及它是否来自历史重放的标记;函数先清掉用于避免重复显示用户消息的临时记录,然后查看这一轮的状态。成功时清掉旧错误并标记任务完成;被打断时判断是不是因为预算限制,再显示合适原因;失败时避免重复显示同一个错误,或者在没有错误详情时正常收尾并尝试发送排队中的下一条输入。它不返回值,但会改动错误记录、任务生命周期和界面刷新状态。

调用关系:它只由 ChatWidget::handle_server_notification 在收到 TurnCompleted 这类通知时调用。它属于总分拣之后的专门收尾步骤,负责把“一轮结束”这种大事件转成用户能看懂的界面状态。

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

ChatWidget::handle_item_started_notification279–323 ↗
fn handle_item_started_notification(
        &mut self,
        notification: ItemStartedNotification,
        from_replay: bool,
    )

作用:这个函数处理“一轮对话里的某个小项目开始了”。小项目可能是运行命令、改文件、联网搜索、生成图片、调用工具,或者进入审查模式。

数据流:进去的是一个项目开始通知,以及它是不是历史重放;函数查看项目类型,然后让界面加上对应的开始状态,比如命令执行卡片、文件修改提示、网页搜索提示、图片生成提示或协作代理活动。对于进入审查模式,它会避免在历史重放时重复触发真实的模式切换。它不返回值,但会在聊天窗口里创建或更新对应的显示块。

调用关系:它由 ChatWidget::handle_server_notification 在收到 ItemStarted 通知时调用。它是大分拣员下面的“小项目开工登记处”,把不同种类的项目开工消息送到聊天界面的具体显示逻辑。

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

ChatWidget::handle_item_completed_notification325–335 ↗
fn handle_item_completed_notification(
        &mut self,
        notification: ItemCompletedNotification,
        replay_kind: Option<ReplayKind>,
    )

作用:这个函数处理“一轮对话里的某个小项目完成了”。它把完成后的项目交给统一的渲染流程,让聊天记录里出现最终结果。

数据流:进去的是项目完成通知,以及可选的历史重放标记;函数取出完成的项目内容和所属轮次编号,并把来源标成实时消息或重放消息;然后交给统一的项目处理入口显示。它不返回值,但会把这个完成项目变成聊天界面里的最终内容。

调用关系:它由 ChatWidget::handle_server_notification 在收到 ItemCompleted 通知时调用。相比“项目开始”的函数负责开场,它负责收尾,把完成内容交给后续统一处理,保证实时消息和历史重放消息都能用同一套显示规则。

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

tui/src/chatwidget/protocol_requests.rs源码 ↗
orchestrationrequest handling

ChatWidget 是终端聊天界面里的核心部件。后台服务器会不断发来不同类型的消息:有的要用户批准执行命令,有的要批准改文件,有的要用户补充输入,有的是安全审查结果。这个文件做的事很像前台接线员:先看消息类型,再转交给界面里对应的流程。如果某些路径从服务器格式转换成本机路径失败,它会直接在界面里显示错误,免得用户以为系统没反应。还有一些当前终端界面暂时不支持的请求,它会显示一个“这里还没实现”的提示;但如果是在回放旧记录,就不打扰用户。整体上,它不负责真正执行命令或修改文件,而是负责把服务器语言变成界面动作。

函数细节7
ChatWidget::handle_server_request9–57 ↗
fn handle_server_request(
        &mut self,
        request: ServerRequest,
        replay_kind: Option<ReplayKind>,
    )

作用:接收后台服务器发来的一个请求,然后按请求种类分发到聊天界面的对应流程。它让用户能在终端里看到命令审批、文件修改审批、权限申请、工具输入等需要自己决定的事情。

数据流:进去的是一个 ServerRequest,也就是服务器消息,外加一个可选的 replay_kind,用来表示这是不是在回放旧事件。函数先取出请求编号,再查看请求属于哪一类:命令审批会变成执行审批事件,文件修改审批会变成补丁审批事件,权限申请会尝试把路径转换成本机能看的路径,工具输入会交给用户输入流程。转换失败时,它会往界面加入一条错误消息;遇到当前没实现的请求时,如果不是回放,就加入一条占位错误提示。出来没有单独返回值,但 ChatWidget 的界面状态会被更新。

调用关系:这是服务器请求进入 ChatWidget 后的主要入口。它会用请求的 id 来追踪这次请求,也会在出错时用格式化文字生成用户能看到的错误消息。真正显示审批、权限和输入界面的工作,会被它转交给 ChatWidget 里的其他专门流程。

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

ChatWidget::handle_skills_list_response59–61 ↗
fn handle_skills_list_response(&mut self, response: SkillsListResponse)

作用:接收“技能列表”的服务器响应,并把它交给聊天界面显示或使用。这里的技能可以理解成系统知道自己能做哪些事的清单。

数据流:进去的是 SkillsListResponse,也就是服务器返回的技能列表数据。函数不改写这份数据,直接把它交给 on_list_skills。出来没有返回值,但界面内部会根据这份列表更新对应内容。

调用关系:它是技能列表响应到达 ChatWidget 后的中转点。它自己不解释技能内容,只负责把响应交给真正展示或保存技能列表的内部流程。

ChatWidget::on_patch_apply_output_delta63–63 ↗
fn on_patch_apply_output_delta(&mut self, _item_id: String, _delta: String)

作用:这是给“应用补丁时产生的增量输出”预留的接口,但目前什么也不做。可以把它看成一个先留好的插座,未来需要显示补丁输出时再接上电器。

数据流:进去的是补丁相关项目编号 item_id 和一小段新增输出 delta。当前函数把两个输入都忽略,不修改界面,也不返回内容。

调用关系:当系统将来想把补丁应用过程中的实时输出送到 ChatWidget 时,会走到这个位置。现在它没有把活儿交给任何其他函数,也不会影响用户界面。

ChatWidget::on_guardian_review_notification65–153 ↗
fn on_guardian_review_notification(
        &mut self,
        id: String,
        turn_id: String,
        started_at_ms: i64,
        review: codex_app_server_protocol::GuardianApprovalReview,

作用:接收后台发来的“守护审查”通知,并把它翻译成聊天界面内部统一的安全评估事件。守护审查可以理解成系统在替用户检查某个操作是否安全、风险有多高。

数据流:进去的是审查编号、对话轮次编号、开始时间、审查结果、可选的完成时间和决策来源,以及被审查的动作。函数先尝试把动作里的路径等信息转换成本机格式;失败就显示错误并停止。成功后,它把服务器里的状态、风险等级、用户授权等级、审查理由和决策来源,逐项换成界面内部使用的类型。最后生成一个 GuardianAssessmentEvent,并交给 ChatWidget 更新界面。出来没有返回值,但用户会看到安全审查的进度或结果。

调用关系:这是守护审查通知进入终端界面的翻译层。它会调用 try_into 来把服务器动作转换成本地动作,失败时用格式化错误消息告诉用户;转换成功后,把整理好的评估事件交给 ChatWidget 的安全评估展示流程。

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

ChatWidget::on_shutdown_complete155–157 ↗
fn on_shutdown_complete(&mut self)

作用:在后台通知“关闭已经完成”时,让终端界面立刻退出。它避免用户卡在一个其实已经结束的界面里。

数据流:进去没有额外数据。函数直接请求 ChatWidget 立即退出。出来没有返回值,但程序的下一步会进入退出流程。

调用关系:它位于关闭流程的末尾:后台确认收尾完成后,这个函数把消息转成界面的退出请求。具体怎么退出由 ChatWidget 的退出机制继续完成。

ChatWidget::on_turn_diff159–162 ↗
fn on_turn_diff(&mut self, unified_diff: String)

作用:收到某一轮对话产生的差异内容时,记录调试信息,并刷新状态栏。这里的 diff 指两份文本或文件之间的差别。

数据流:进去的是 unified_diff,也就是一段标准格式的差异文本。函数把它写进调试日志,然后刷新界面底部或顶部的状态栏。它不直接把差异正文显示给用户,也不返回内容。

调用关系:它在收到对话轮次差异通知时被触发。它会使用 debug 日志记录这段差异,方便开发者排查问题;随后让 ChatWidget 更新状态栏,让界面状态保持新鲜。

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

ChatWidget::on_deprecation_notice164–167 ↗
fn on_deprecation_notice(&mut self, summary: String, details: Option<String>)

作用:显示“某功能将被弃用”的提醒。弃用就是这个功能以后可能不再推荐使用,甚至会被移除,所以用户需要知道。

数据流:进去的是一段摘要 summary 和可选的详细说明 details。函数先把它们做成一条历史记录里的提示卡片,然后加入聊天历史,最后请求界面重画。出来没有返回值,但用户会在聊天界面里看到这条弃用提醒。

调用关系:它在收到弃用通知时工作。它借助 new_deprecation_notice 生成适合放进聊天历史的显示单元,再让 ChatWidget 把这条消息加入历史并刷新屏幕。

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

选择和操作弹窗

构建模型、评审、计划和连接器浏览的弹窗流程,让用户选择后续操作。

tui/src/chatwidget/connectors.rs源码 ↗
orchestrationcross-cutting:用户打开 Apps 弹窗、后台预取连接器、连接器列表返回、用户启用或禁用应用时都会活跃

这个文件可以理解成聊天窗口里“应用商店小面板”的后台管家。用户按下 /apps 或用 $ 想插入某个应用时,界面不能每次都傻等网络结果,所以它先看功能是否开启、用户是否有 ChatGPT 账号,再决定要不要去拉取应用列表。拉取过程中,它会记住当前状态:还没开始、正在加载、已经拿到列表、或者失败了。如果列表还没回来,就先打开一个“正在加载 Apps”的弹窗;如果已经有列表,就显示可搜索的应用选择框;如果失败,就把错误写到聊天历史里。它还处理一个容易出错的情况:用户连续要求刷新时,不会同时发起一堆请求,而是先标记“等当前请求结束后再强制刷新”。另外,拿到新列表后,它会尽量保留用户本地看到的启用/禁用状态,并在弹窗开着时直接替换内容,让用户不用关掉重开。

函数细节17
ChatWidget::refresh_connectors24–26 ↗
fn refresh_connectors(&mut self, force_refetch: bool)

作用:这是对外提供的“刷新 Apps 列表”按钮。别人调用它时,只要告诉它是否要强制重新拉取,它就把真正的刷新安排下去。

数据流:输入是一个 force_refetch 标记,意思是“要不要绕过旧数据重新拿”。它不自己下载列表,而是把这个要求交给 queue_connectors_refresh。结果是:如果条件允许,后面会发出获取 Apps 列表的事件;它本身不直接返回列表。

调用关系:它是外部代码想主动刷新连接器时进入这个文件的入口之一。它把活儿交给 ChatWidget::queue_connectors_refresh,由后者判断能不能发起请求。

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

ChatWidget::prefetch_connectors28–30 ↗
fn prefetch_connectors(&mut self)

作用:这是“提前偷偷加载 Apps 列表”的入口。用户还没真正打开列表时,系统可以先准备好,等用户打开时更快。

数据流:它没有额外输入,只固定用 force_refetch=false,表示能用缓存就别强刷。它把请求交给 queue_connectors_refresh,之后可能触发后台拉取。输出不是数据,而是改变内部加载状态并可能发出获取事件。

调用关系:它通常用于提前准备数据。它调用 ChatWidget::queue_connectors_refresh,和手动刷新走同一条路,只是不会强制刷新。

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

ChatWidget::queue_connectors_refresh32–37 ↗
fn queue_connectors_refresh(&mut self, force_refetch: bool)

作用:这个函数是“排队发起 Apps 列表刷新”的门卫。它先问能不能开始刷新,能的话才向应用事件通道发送获取列表的命令。

数据流:输入是 force_refetch。它先调用 begin_connectors_refresh 检查功能是否可用、当前是否已有请求在跑,并更新内部状态。若允许,它通过 app_event_tx 发出 AppEvent::FetchConnectorsList,告诉系统去拿连接器列表;若不允许,就什么也不发。

调用关系:它被 ChatWidget::refresh_connectors、ChatWidget::prefetch_connectors、ChatWidget::add_connectors_output 和 ChatWidget::on_connectors_loaded 调用,是所有刷新请求的统一出口。它把判断工作交给 ChatWidget::begin_connectors_refresh,把真正抓取工作交给外部事件系统。

调用图:调用 1 个内部函数(begin_connectors_refresh);被 4 处调用(add_connectors_output, on_connectors_loaded, prefetch_connectors, refresh_connectors)。

ChatWidget::begin_connectors_refresh39–55 ↗
fn begin_connectors_refresh(&mut self, force_refetch: bool) -> bool

作用:这个函数决定“现在能不能开始一次新的 Apps 刷新”。它避免功能没开时乱请求,也避免已有请求没结束时重复发请求。

数据流:输入是 force_refetch。它读取配置、账号状态和当前 connectors 的加载标记。若 Apps 功能不可用,就返回 false;若已有请求在路上,强制刷新时只记一个待办标记,然后返回 false;若可以开始,就把 prefetch_in_flight 设为 true,并在没有现成列表时把缓存状态改成 Loading,最后返回 true。

调用关系:它只被 ChatWidget::queue_connectors_refresh 调用,是刷新流程的安全检查点。它会调用 ChatWidget::connectors_enabled 来确认用户有没有资格使用 Apps。

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

ChatWidget::connectors_enabled57–59 ↗
fn connectors_enabled(&self) -> bool

作用:这个函数回答一个很基础的问题:当前用户能不能用 Apps。它同时检查功能开关和账号条件。

数据流:它不接收参数,只读取配置里的 Apps 功能开关,以及 has_chatgpt_account 这个账号标记。两者都满足就返回 true,否则返回 false;不修改任何状态。

调用关系:它被 ChatWidget::begin_connectors_refresh、ChatWidget::add_connectors_output 和 ChatWidget::connectors_for_mentions 使用。也就是说,无论是刷新、打开弹窗,还是给输入提示提供应用名,都先过这道资格检查。

调用图:被 3 处调用(add_connectors_output, begin_connectors_refresh, connectors_for_mentions)。

ChatWidget::connectors_for_mentions61–74 ↗
fn connectors_for_mentions(&self) -> Option<&[AppInfo]>

作用:这个函数给“输入时用 $ 提到应用”的功能提供可用的 Apps 列表。它只在 Apps 可用且已经有数据时返回列表。

数据流:它不接收参数,先调用 connectors_enabled 检查功能是否可用。然后优先读取 partial_snapshot,也就是先到的一部分列表;如果没有部分列表,再看正式缓存 Ready 里的列表。能找到就返回应用切片,找不到就返回 None。

调用关系:它服务于输入提示或提及功能,不负责拉取数据。它依赖 ChatWidget::connectors_enabled 和当前缓存状态,把已经准备好的数据借给别的界面逻辑使用。

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

ChatWidget::add_connectors_output76–106 ↗
fn add_connectors_output(&mut self)

作用:这是用户打开 Apps 列表时的主要入口。它决定应该显示“功能没开”、错误、加载中,还是完整的应用选择弹窗。

数据流:它先检查 connectors_enabled。若不可用,就在聊天里加一条提示。若可用,它复制当前缓存状态,决定是否需要强制刷新,并调用 queue_connectors_refresh。接着根据旧缓存:有列表就打开 Apps 弹窗,列表为空就提示没有应用;失败就写错误历史;还在加载或没初始化就打开加载弹窗。最后请求界面重绘。

调用关系:它通常由用户命令触发,比如 /apps。它会调用 ChatWidget::queue_connectors_refresh 发起或补发刷新,也会调用 ChatWidget::open_connectors_popup 或 ChatWidget::open_connectors_loading_popup 来更新底部面板。

调用图:调用 4 个内部函数(connectors_enabled, open_connectors_loading_popup, open_connectors_popup, queue_connectors_refresh);外部调用 2 个(new_error_event, matches!)。

ChatWidget::open_connectors_loading_popup108–116 ↗
fn open_connectors_loading_popup(&mut self)

作用:这个函数打开“Apps 正在加载”的小弹窗。它的作用是让用户知道系统正在取列表,而不是界面没反应。

数据流:它不接收参数。它通过 connectors_loading_popup_params 生成弹窗内容,然后尝试替换当前已打开的同一个 Apps 选择视图;如果没有这个视图,就新开一个。结果是底部面板显示加载中的占位内容。

调用关系:它被 ChatWidget::add_connectors_output 调用,专门处理列表还没准备好时的界面展示。它把弹窗参数的制作交给 ChatWidget::connectors_loading_popup_params。

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

ChatWidget::open_connectors_popup118–122 ↗
fn open_connectors_popup(&mut self, connectors: &[AppInfo])

作用:这个函数打开真正的 Apps 选择弹窗,把可安装或已安装的应用列给用户看。

数据流:输入是一组 AppInfo,也就是应用信息。它调用 connectors_popup_params 把这些信息变成底部选择框需要的标题、列表项、搜索设置和操作按钮,然后让 bottom_pane 显示出来。

调用关系:它被 ChatWidget::add_connectors_output 在缓存已经 Ready 时调用。它不自己整理每个应用的显示细节,而是交给 ChatWidget::connectors_popup_params。

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

ChatWidget::connectors_loading_popup_params124–140 ↗
fn connectors_loading_popup_params(&self) -> SelectionViewParams

作用:这个函数制作“加载中”弹窗的内容说明。它定义标题、提示文字和一个不可点击的占位项。

数据流:它不接收参数。它创建一个标题区域,写上 Apps 和“正在加载已安装及可用应用”。然后返回 SelectionViewParams,里面只有一个禁用的列表项“Loading apps...”。它不直接显示界面,只产出界面配置。

调用关系:它被 ChatWidget::open_connectors_loading_popup 调用。可以把它看成做菜单模板的人,真正把菜单挂到界面上的是 open_connectors_loading_popup。

调用图:调用 1 个内部函数(new);被 1 处调用(open_connectors_loading_popup);外部调用 4 个(new, default, from, vec!)。

ChatWidget::connectors_popup_params142–239 ↗
fn connectors_popup_params(
        &self,
        connectors: &[AppInfo],
        selected_connector_id: Option<&str>,
    ) -> SelectionViewParams

作用:这个函数把一批 AppInfo 应用数据加工成“可搜索、可选择、可点击”的 Apps 弹窗配置。它决定每一行显示什么、按回车会做什么。

数据流:输入是 connectors 列表,以及一个可选的 selected_connector_id,用来尽量保持用户之前选中的那一项。它统计总应用数和已安装数,生成标题;再逐个应用生成列表项,包括显示名、简介、搜索关键词、安装状态。如果应用有 install_url,选择它会发出 OpenAppLink 事件,在浏览器打开管理或安装页面;如果没有链接,选择它会在历史里插入一条提示。输出是完整的 SelectionViewParams。

调用关系:它被 ChatWidget::open_connectors_popup 和 ChatWidget::refresh_connectors_popup_if_open 调用,是 Apps 弹窗内容的主要组装器。它还使用 ChatWidget::connector_brief_description、ChatWidget::connector_description 和 ChatWidget::connector_status_label 来生成好懂的文字。

调用图:调用 2 个内部函数(connector_display_label, new);被 2 处调用(open_connectors_popup, refresh_connectors_popup_if_open);外部调用 11 个(new, default, from, connector_brief_description, connector_description, connector_status_label, with_capacity, iter, len, format! (+1 more))。

ChatWidget::refresh_connectors_popup_if_open241–259 ↗
fn refresh_connectors_popup_if_open(&mut self, connectors: &[AppInfo])

作用:这个函数在 Apps 弹窗已经打开时,用新列表悄悄替换旧内容。它尽量保留用户当前选中的应用,避免刷新后光标跳走。

数据流:输入是新的 connectors 列表。它先问 bottom_pane 当前 Apps 视图选中了第几项,再从旧缓存里找出对应应用的 id。然后用 connectors_popup_params 生成新弹窗配置,并尝试只在该视图正打开时替换它。结果是界面内容更新,但用户选择位置尽量保持。

调用关系:它被 ChatWidget::on_connectors_loaded 在新数据返回后调用,也被 ChatWidget::update_connector_enabled 在启用状态变化后调用。它本身不拉数据,只负责把新状态反映到已经打开的弹窗上。

调用图:调用 1 个内部函数(connectors_popup_params);被 2 处调用(on_connectors_loaded, update_connector_enabled)。

ChatWidget::connector_brief_description261–267 ↗
fn connector_brief_description(connector: &AppInfo) -> String

作用:这个函数给一个应用生成列表里用的一行简短说明。它会把安装状态和应用简介合在一起。

数据流:输入是一个 AppInfo。它先用 connector_status_label 得到状态,比如“Installed”或“Can be installed”;再用 connector_description 取干净的简介。如果有简介,就输出“状态 · 简介”;如果没有,就只输出状态。

调用关系:它被 ChatWidget::connectors_popup_params 用来填每个应用列表项的 description。它把状态判断和简介清洗两个小步骤组合起来。

调用图:外部调用 3 个(connector_description, connector_status_label, format!)。

ChatWidget::connector_status_label269–279 ↗
fn connector_status_label(connector: &AppInfo) -> &'static str

作用:这个函数把应用的安装和启用状态翻译成人能看懂的短标签。比如已安装、已安装但禁用、可安装。

数据流:输入是一个 AppInfo。它读取 is_accessible 和 is_enabled:如果可访问且启用,返回 Installed;可访问但未启用,返回 Installed · Disabled;不可访问则返回 Can be installed。它不修改数据。

调用关系:它被 ChatWidget::connector_brief_description 和 ChatWidget::connectors_popup_params 使用。所有 Apps 弹窗里关于状态的文字都靠它保持一致。

ChatWidget::connector_description281–288 ↗
fn connector_description(connector: &AppInfo) -> Option<String>

作用:这个函数取出应用简介,并把空白内容过滤掉。这样弹窗不会显示一段空字符串或全是空格的说明。

数据流:输入是一个 AppInfo。它读取 description 字段,先去掉前后空格;如果结果为空,就返回 None;否则返回整理后的字符串。它不改原来的 AppInfo。

调用关系:它被 ChatWidget::connector_brief_description 和 ChatWidget::connectors_popup_params 使用。它负责把外部来的简介文字清理干净,再交给弹窗显示。

ChatWidget::on_connectors_loaded290–350 ↗
fn on_connectors_loaded(
        &mut self,
        result: Result<ConnectorsSnapshot, String>,
        is_final: bool,
    )

作用:这是 Apps 列表加载完成后进入聊天界面的处理函数。它要处理成功、失败、部分结果、最终结果,还要决定是否补发之前排队的强制刷新。

数据流:输入是 result,也就是成功得到 ConnectorsSnapshot 或失败字符串,以及 is_final,表示这是不是最终结果。如果是最终结果,它会结束 in-flight 标记,并检查是否有待执行的强制刷新。成功时,它会尽量保留旧列表里的启用状态;最终结果会写入正式缓存并刷新弹窗,非最终结果会放进 partial_snapshot。失败时,如果已有旧缓存,就保留旧数据;如果有部分列表,就退回到部分列表;如果什么都没有,就把缓存设为 Failed。最后如果之前有人要求强制刷新,它会再次调用 queue_connectors_refresh。

调用关系:它由外部获取 Apps 列表的流程在数据回来后调用。它会调用 ChatWidget::refresh_connectors_popup_if_open 更新打开的界面,也可能调用 ChatWidget::queue_connectors_refresh 继续处理排队的强制刷新。

调用图:调用 2 个内部函数(queue_connectors_refresh, refresh_connectors_popup_if_open);外部调用 3 个(Failed, Ready, warn!)。

ChatWidget::update_connector_enabled352–373 ↗
fn update_connector_enabled(&mut self, connector_id: &str, enabled: bool)

作用:这个函数在某个应用的启用或禁用状态变化后,更新本地缓存和已打开的弹窗。这样用户刚操作完,界面马上跟着变。

数据流:输入是 connector_id 和 enabled。它先要求当前缓存必须是 Ready,否则直接返回。然后在快照里找到对应应用,比较状态有没有变化;如果没变就不做事。如果变了,就改掉该应用的 is_enabled,刷新已打开的 Apps 弹窗,把新快照写回缓存,并同步给 bottom_pane。

调用关系:它通常在外部确认某个应用启用状态改变后被调用。它把界面刷新交给 ChatWidget::refresh_connectors_popup_if_open,并把最终状态同步到连接器缓存和底部面板。

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

tui/src/chatwidget/model_popups.rs源码 ↗
orchestrationuser interaction / popup handling

这个文件像聊天界面的“模型设置菜单”。用户输入或触发选模型时,它先检查程序是否已经启动完,再从模型目录拿到可选模型,分成快捷的 auto 模型和完整模型列表。选普通模型时,还会继续问“推理强度”,也就是让模型思考得多深。比较特别的是 Plan 模式:这是协作模式里的规划阶段,某些推理设置可能只该影响 Plan,也可能要影响所有模式,所以文件会在容易误改全局默认值时多弹一个确认菜单。它还会在用户用了自定义 OpenAI 地址时提示风险,因为模型列表可能不兼容。真正改设置不是这里直接改,而是发出 AppEvent(应用内部事件,像递纸条让主程序去执行)。

函数细节16
ChatWidget::open_model_popup11–31 ↗
fn open_model_popup(&mut self)

作用:打开“选择模型”的入口。它先确认聊天会话已经准备好,再读取当前可用模型;如果还没准备好或模型列表正在更新,就给用户一条提示。

数据流:进去的是当前界面状态和模型目录 → 它检查启动状态,尝试取出模型预设列表 → 成功后把列表交给后续弹窗;失败时不打开菜单,只在聊天里显示说明。

调用关系:这是用户触发选模型时的第一站。它不自己组装复杂菜单,而是在拿到模型列表后交给 ChatWidget::open_model_popup_with_presets 继续处理。

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

ChatWidget::model_menu_header33–43 ↗
fn model_menu_header(&self, title: &str, subtitle: &str) -> Box<dyn Renderable>

作用:做模型选择弹窗顶部的标题区域。它把标题、副标题和可能出现的警告拼成一个可显示的小块。

数据流:进去的是标题和副标题文字 → 它创建一个竖排显示区域,把标题加粗、副标题变淡,再视情况加一行红色警告 → 出来的是弹窗可以直接展示的头部组件。

调用关系:它是多个模型菜单共用的“抬头模板”。ChatWidget::open_model_popup_with_presets 和 ChatWidget::open_all_models_popup 都会用它,让不同弹窗看起来一致。

调用图:调用 2 个内部函数(model_menu_warning_line, new);被 2 处调用(open_all_models_popup, open_model_popup_with_presets);外部调用 2 个(new, from)。

ChatWidget::model_menu_warning_line45–51 ↗
fn model_menu_warning_line(&self) -> Option<Line<'static>>

作用:决定模型菜单里要不要显示红色警告。警告内容是:如果 OpenAI 的服务地址被改过,选模型可能不一定正常。

数据流:进去的是当前配置 → 它询问 ChatWidget::custom_openai_base_url 有没有发现自定义地址 → 如果有,就生成一行红色警告;没有就什么也不返回。

调用关系:它只服务于 ChatWidget::model_menu_header。头部组件在生成时会问它是否需要额外提醒用户。

调用图:调用 1 个内部函数(custom_openai_base_url);被 1 处调用(model_menu_header);外部调用 2 个(from, format!)。

ChatWidget::custom_openai_base_url53–70 ↗
fn custom_openai_base_url(&self) -> Option<String>

作用:检查当前是不是用了非官方默认的 OpenAI 地址。这个判断用来提醒用户:模型选择功能可能和自定义服务不完全兼容。

数据流:进去的是配置里的模型服务商和 base_url → 它先确认服务商是 OpenAI,再去掉空格和末尾斜杠做比较 → 如果地址存在且不是默认地址,就返回原地址;否则返回空。

调用关系:它是一个小判断工具,被 ChatWidget::model_menu_warning_line 用来决定是否显示风险提示。

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

ChatWidget::open_model_popup_with_presets72–155 ↗
fn open_model_popup_with_presets(&mut self, presets: Vec<ModelPreset>)

作用:根据已有模型列表打开快捷模型菜单。它优先展示 codex-auto 这类“自动模式”,并提供进入“所有模型”的入口。

数据流:进去的是一批模型预设 → 它过滤掉不该显示的项,找出当前模型,拆分快捷 auto 模型和其他模型,给每个选项配好点击后的动作 → 最后把选择列表交给底部面板显示;如果没有 auto 模型,就直接打开完整模型列表。

调用关系:它接在 ChatWidget::open_model_popup 后面,是模型菜单的主要装配者。它会调用 ChatWidget::model_menu_header 做菜单头,也可能把流程转给 ChatWidget::open_all_models_popup。

调用图:调用 2 个内部函数(model_menu_header, open_all_models_popup);被 1 处调用(open_model_popup);外部调用 3 个(default, format!, vec!)。

ChatWidget::is_auto_model157–159 ↗
fn is_auto_model(model: &str) -> bool

作用:判断一个模型名是不是快捷 auto 模型。这里的规则很简单:名字以“codex-auto-”开头就算。

数据流:进去的是一个模型名字符串 → 它检查前缀 → 出来的是是或否的结果,不改任何状态。

调用关系:它帮助 ChatWidget::open_model_popup_with_presets 把模型分组:一组放在快捷菜单里,另一组放到“所有模型”里。

ChatWidget::auto_model_order161–168 ↗
fn auto_model_order(model: &str) -> usize

作用:给快捷 auto 模型排显示顺序。它让 fast、balanced、thorough 按固定顺序出现,而不是随模型目录的顺序乱排。

数据流:进去的是模型名 → 它把已知名字映射成数字顺序 → 出来的是排序用的编号;不认识的 auto 模型放在最后。

调用关系:它被快捷模型菜单用来排序,让用户每次看到的顺序稳定,减少误选。

ChatWidget::open_all_models_popup170–214 ↗
fn open_all_models_popup(&mut self, presets: Vec<ModelPreset>)

作用:打开完整模型列表。用户在这里可以选择具体模型,并通常会进入下一步选择推理强度。

数据流:进去的是一批非快捷模型预设 → 如果为空,就显示“暂无更多模型”;否则每个模型变成一个可点选项,点击后发送打开推理强度弹窗的事件 → 最后显示完整选择菜单。

调用关系:它通常由 ChatWidget::open_model_popup_with_presets 里的“All models”入口带出来,也可以在没有快捷模型时直接接管流程。它还复用 ChatWidget::model_menu_header 做顶部说明。

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

ChatWidget::model_selection_actions216–237 ↗
fn model_selection_actions(
        model_for_action: String,
        effort_for_action: Option<ReasoningEffortConfig>,
        should_prompt_plan_mode_scope: bool,
    ) -> Vec<SelectionAction>

作用:为快捷模型生成“点了以后该做什么”的动作。它把一次点击包装成一串内部事件。

数据流:进去的是要选的模型、默认推理强度、以及是否需要先问 Plan 模式范围 → 如果需要确认范围,就只发出打开确认弹窗的事件;否则发出更新模型、更新推理强度、保存选择这三类事件 → 出来的是可挂到菜单项上的动作列表。

调用关系:它是快捷模型选项背后的执行按钮。ChatWidget::open_model_popup_with_presets 生成菜单项时会用它,真正执行时再由应用事件系统接手。

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

ChatWidget::should_prompt_plan_mode_reasoning_scope239–257 ↗
fn should_prompt_plan_mode_reasoning_scope(
        &self,
        selected_model: &str,
        selected_effort: Option<ReasoningEffortConfig>,
    ) -> bool

作用:判断在 Plan 模式下改推理强度时,要不要多问一句“只改 Plan,还是改所有模式”。这是为了防止用户无意中覆盖全局默认设置。

数据流:进去的是用户选中的模型和推理强度,以及当前协作模式、当前模型、当前 Plan 设置 → 它先排除非 Plan 模式、未启用协作模式、换了模型等情况,再比较这次选择是否真的会改变 Plan 或全局默认 → 出来的是是否需要弹确认框。

调用关系:它是 Plan 模式的安全闸门。ChatWidget::open_reasoning_popup 等选择流程会先问它,如果答案是需要确认,就把用户导向 ChatWidget::open_plan_reasoning_scope_prompt。

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

ChatWidget::open_plan_reasoning_scope_prompt259–348 ↗
fn open_plan_reasoning_scope_prompt(
        &mut self,
        model: String,
        effort: Option<ReasoningEffortConfig>,
    )

作用:打开 Plan 模式专用确认弹窗。它让用户选择:这次推理强度只用于 Plan 模式,还是同时改成所有模式的默认值。

数据流:进去的是模型和推理强度 → 它把推理强度写成人能读懂的话,说明当前 Plan 默认值来源,再生成两个选项及各自事件 → 出来的是一个显示在底部面板的选择弹窗,同时发出通知提醒用户正在做 Plan 范围选择。

调用关系:它接在安全判断之后出现。前面的模型或推理强度菜单发现可能影响 Plan 设置时,会把流程交给它;用户选完后,它通过 AppEvent 让主程序更新并保存对应范围的设置。

调用图:调用 1 个内部函数(plan_mask);外部调用 3 个(default, format!, vec!)。

ChatWidget::open_reasoning_popup351–498 ↗
fn open_reasoning_popup(&mut self, preset: ModelPreset)

作用:给某个模型打开“选择推理强度”的弹窗。推理强度可以理解为模型回答前愿意花多少力气思考。

数据流:进去的是一个模型预设,里面有默认推理强度和支持的强度列表 → 它整理可选项,找出默认项和当前应高亮的项,给高消耗选项加警告;如果只有一个选项就直接应用或进入 Plan 确认 → 如果有多个选项,就显示选择弹窗。

调用关系:这是完整模型选择后的第二阶段。ChatWidget::open_all_models_popup 的模型项会打开它;它可能调用 ChatWidget::should_prompt_plan_mode_reasoning_scope 做安全判断,也可能调用 ChatWidget::apply_model_and_effort 直接应用选择。

调用图:调用 3 个内部函数(apply_model_and_effort, should_prompt_plan_mode_reasoning_scope, new);外部调用 7 个(new, default, from, reasoning_effort_label, new, format!, vec!)。

ChatWidget::reasoning_effort_label500–510 ↗
fn reasoning_effort_label(effort: &ReasoningEffortConfig) -> String

作用:把内部的推理强度值变成菜单上能看懂的短标签,比如 Low、Medium、Extra high。

数据流:进去的是一个推理强度配置 → 它按固定对应关系转成显示文字,自定义值就原样显示 → 出来的是一段给界面用的字符串。

调用关系:它是显示文字的小翻译器。推理强度弹窗和说明文字需要把配置值展示给人看时会用到它。

ChatWidget::reasoning_effort_sentence_label512–517 ↗
fn reasoning_effort_sentence_label(effort: &ReasoningEffortConfig) -> String

作用:把推理强度变成适合放进句子里的文字。比如菜单标签是“High”,句子里会变成“high reasoning”。

数据流:进去的是一个推理强度配置 → 如果是自定义值就保持原样,否则先用 ChatWidget::reasoning_effort_label 得到标签再转成小写 → 出来的是更适合说明文字的字符串。

调用关系:它主要服务于 ChatWidget::open_plan_reasoning_scope_prompt,用来生成自然一点的提示语。

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

ChatWidget::apply_model_and_effort_without_persist519–527 ↗
fn apply_model_and_effort_without_persist(
        &self,
        model: String,
        effort: Option<ReasoningEffortConfig>,
    )

作用:临时应用模型和推理强度,但不保存到配置里。也就是说这次运行里先改,重启后不一定保留。

数据流:进去的是模型名和可选推理强度 → 它通过应用事件发送“更新模型”和“更新推理强度” → 出来没有直接返回值,但会让主程序收到更新请求;它不写入持久配置。

调用关系:它是实际更新界面状态的底层小步骤。ChatWidget::apply_model_and_effort 会先调用它,再额外发保存事件。

调用图:被 1 处调用(apply_model_and_effort);外部调用 2 个(UpdateModel, UpdateReasoningEffort)。

ChatWidget::apply_model_and_effort529–533 ↗
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>)

作用:应用模型和推理强度,并保存成以后默认使用的选择。适合用户明确完成选择时调用。

数据流:进去的是模型名和推理强度 → 它先调用 ChatWidget::apply_model_and_effort_without_persist 让当前状态立刻改变,再发送保存选择的事件 → 结果是当前会话更新,并请求把选择持久化。

调用关系:它是推理强度选择流程的收尾动作之一。ChatWidget::open_reasoning_popup 在不需要 Plan 范围确认时会调用它完成设置。

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

tui/src/chatwidget/review_popups.rs源码 ↗
orchestrationuser interaction

这个文件解决的是一个很实际的问题:用户想让程序帮忙做代码审查时,不能只给一个空入口,得让人清楚地选“审查什么”。它给 ChatWidget,也就是聊天界面的主要组件,加了几种弹窗。第一个弹窗像菜单,列出几种审查方式。选“基于分支审查”时,会继续打开分支选择器;选“审查某个提交”时,会打开提交选择器;选“未提交改动”会直接开始审查;选“自定义要求”会打开一个输入框。这里的 SelectionItem 可以理解成菜单里的一行,里面写着显示文字和用户按下回车后要做的事。AppEvent 是应用内部传话用的消息,像把小纸条递给主程序:“请打开分支选择器”或“请开始审查这个目标”。这个文件本身不做真正的代码审查,它负责把用户的选择变成清楚的审查目标。

函数细节5
ChatWidget::open_review_popup6–61 ↗
fn open_review_popup(&mut self)

作用:打开最外层的“选择审查方式”弹窗。用户从这里决定是审查分支差异、未提交改动、某个提交,还是输入自己的审查要求。

数据流:进去的是当前聊天界面里的状态,主要会读取当前工作目录 cwd,因为后面找分支或提交需要知道在哪个代码仓库里找。它组装出四个菜单项,每一项都带着用户选中后要发出的动作。出来的结果是底部面板显示一个选择弹窗;如果用户点选某一项,后续会发送对应的应用事件,或者直接发起一次审查。

调用关系:它是审查弹窗流程的入口。用户触发“review”相关操作时会先来到这里。它本身只搭好菜单,不深入查 Git 分支或提交;如果用户选择分支或提交,它把活交给后续的分支选择器或提交选择器;如果选择未提交改动,则直接通过事件发送器请求开始审查。

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

ChatWidget::show_review_branch_picker63–93 ↗
async fn show_review_branch_picker(&mut self, cwd: &Path)

作用:显示一个本地 Git 分支选择弹窗,让用户选一个“基准分支”来做类似 Pull Request 的代码审查。

数据流:进去的是一个代码仓库路径 cwd。它先读取这个仓库里的本地分支列表,再读取当前所在分支名;如果当前不是正常分支,就显示“detached HEAD”,意思是 Git 当前停在某个提交上而不是分支上。然后它把每个可选分支做成一行,比如“当前分支 -> 目标分支”。出来的结果是一个可搜索的分支选择弹窗;用户选中某个分支后,会生成一个 BaseBranch 审查目标。

调用关系:它通常接在 ChatWidget::open_review_popup 后面:用户先选“Review against a base branch”,应用再打开这个分支选择器。它查到分支后把它们交给底部面板显示;真正选中后,又通过事件发送器把“请按这个基准分支审查”的请求送回应用主流程。

调用图:外部调用 4 个(default, with_capacity, format!, vec!)。

ChatWidget::show_review_commit_picker95–126 ↗
async fn show_review_commit_picker(&mut self, cwd: &Path)

作用:显示最近提交的选择弹窗,让用户挑一个具体 commit,也就是一次代码提交,来做审查。

数据流:进去的是代码仓库路径 cwd。它读取这个仓库最近的提交,最多取 100 条。每条提交会拿出标题 subject 和编号 sha;sha 是 Git 给每次提交的唯一标识,像快递单号。它把这些提交做成可搜索的菜单项。出来的结果是底部面板显示“选择一个提交”的弹窗;用户选中后,会把对应 sha 和标题包装成 Commit 审查目标并发出去。

调用关系:它也是接在 ChatWidget::open_review_popup 后面的二级界面:用户选择“Review a commit”后进入这里。它负责把 Git 提交历史变成用户能看懂、能搜索的列表;选中后不自己审查,而是把具体提交交给应用里的审查流程。

调用图:外部调用 4 个(default, with_capacity, format!, vec!)。

ChatWidget::show_review_custom_prompt128–146 ↗
fn show_review_custom_prompt(&mut self)

作用:打开一个输入框,让用户自己写审查说明,比如“重点看安全问题”或“只检查命名和可读性”。

数据流:进去的是当前聊天界面状态,它会复制一份应用事件发送器,方便稍后提交用户输入。它创建一个 CustomPromptView,也就是自定义文本输入界面。用户输入内容并按回车后,它会先去掉前后空白;如果什么都没写,就什么也不做;如果有内容,就把文字变成 Custom 审查目标发出去。

调用关系:它通常由 ChatWidget::open_review_popup 里的“Custom review instructions”菜单项打开。它和分支、提交选择器不同,不需要查 Git;它只是收集用户的一段文字,然后把这段文字交给统一的审查流程。

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

show_review_commit_picker_with_entries150–182 ↗
fn show_review_commit_picker_with_entries(
    chat: &mut ChatWidget,
    entries: Vec<CommitLogEntry>,
)

作用:这是测试用的辅助函数,用提前准备好的提交列表来显示提交选择弹窗。它让测试不必真的去读 Git 仓库,也能检查弹窗行为是否正确。

数据流:进去的是一个可修改的 ChatWidget 和一组假的或预先准备好的 CommitLogEntry。它逐条取出提交标题和 sha,做成和真实提交选择器一样的菜单项,并设置搜索内容。出来的结果是聊天界面的底部面板显示一个提交选择弹窗;用户或测试代码选中某项时,会发出对应的 Commit 审查目标。

调用关系:它只在测试配置下存在,作用是模仿 ChatWidget::show_review_commit_picker 的界面生成过程。真实函数会去仓库里读取最近提交;这个函数直接使用传进来的 entries,方便测试稳定、快速,不依赖机器上的 Git 状态。

调用图:外部调用 4 个(default, with_capacity, format!, vec!)。

tui/src/chatwidget/plan_implementation.rs源码 ↗
orchestration计划被批准后打开确认弹窗时

这个文件解决的是一个很实际的交接问题:模型刚写完计划,用户同意了,但接下来到底是让模型马上开始改代码,还是换一个干净的新对话再按计划执行,或者继续留在计划模式?这里会组装一个选择菜单。菜单有标题、底部提示文字、三个选项,以及每个选项点下去后要发出的事件。比如“直接实现”会发送一条“Implement the plan.”并切到默认协作模式;“清空上下文后实现”会把已批准的计划包装成一段新的用户意图,再清空界面后提交。文件也会处理不能执行的情况:如果默认模式不可用,相关按钮会变灰;如果没有可用计划,清空上下文的按钮也会说明原因。可以把它理解成前台接待员:不亲自写代码,但负责把用户的选择准确转交给后面的系统。

函数细节1
selection_view_params28–114 ↗
fn selection_view_params(
    default_mask: Option<CollaborationModeMask>,
    plan_markdown: Option<&str>,
    clear_context_usage_label: Option<&str>,
) -> SelectionViewParams

作用:这个函数生成“要不要实现这个计划?”弹窗所需要的完整菜单资料。别人调用它后,就能拿到标题、提示、三个选项,以及每个选项被选中时该触发什么动作。

数据流:进去的是三类信息:默认协作模式是否可用、有没有已批准的计划文本、以及清空上下文时要显示的上下文用量标签。函数先判断“直接实现”能不能用:能用就准备发送一条“Implement the plan.”消息并带上默认模式,不能用就给按钮写上不可用原因。接着判断“清空上下文后实现”能不能用:需要默认模式存在,也需要计划文本不为空;能用就把说明前缀和计划拼成新的用户消息,并准备清空界面后提交。最后它把这些动作、说明文字、禁用原因和底部快捷键提示装进一个 SelectionViewParams 结构里交出去。

调用关系:正常流程里,open_plan_implementation_prompt 会在需要展示确认弹窗时调用它,把生成好的菜单拿去显示给用户。测试函数 plan_implementation_clear_context_requires_default_mode_and_plan 会调用它来确认规则没有写错,尤其是“清空上下文后实现”必须同时有默认模式和计划。函数内部还会调用 standard_popup_hint_line 来取得统一的弹窗底部提示,让这个弹窗和其他弹窗看起来、用起来一致。

调用图:调用 1 个内部函数(standard_popup_hint_line);被 2 处调用(plan_implementation_clear_context_requires_default_mode_and_plan, open_plan_implementation_prompt);外部调用 4 个(default, new, format!, vec!)。

用量和推理控制

实现 token 和用量流程,并提供快速控件来调整模型推理强度。

tui/src/chatwidget/reasoning_shortcuts.rs源码 ↗
domain_logicmain loop / key handling

这个文件解决的是一个很具体的交互问题:用户正在聊天时,想让模型少想一点、更快一点,或多想一点、更仔细一点。它会先确认这个按键真的是推理强度快捷键,而且当前没有弹窗或对话框正在抢键盘输入。然后它查看当前模型支持哪些推理档位,比如低、中、高,再根据当前档位往前或往后挪一格。这里有个重要细节:它不会乱猜模型不支持的档位。如果当前设置不在模型公布的列表里,就先回到默认档位或第一个可用档位。到了最低或最高时,它不会继续改,而是给用户一条提示。普通聊天模式下,修改只影响当前运行状态,不写入永久配置;Plan 模式下,则只改 Plan 模式自己的覆盖设置,避免误改全局设置。

函数细节11
ReasoningShortcutDirection::bound_message32–38 ↗
fn bound_message(self, effort: &ReasoningEffortConfig) -> String

作用:这个函数在用户已经调到最低或最高时,生成一条好懂的提示语。比如“已经是最高级别了”。

数据流:进去的是方向,也就是想调低还是调高,以及当前推理档位 → 它把档位转成人能读懂的文字标签 → 出来的是一段完整提示消息,不改任何状态。

调用关系:它是快捷键处理流程里的边界提示工具。ChatWidget::handle_reasoning_shortcut 发现不能再往上或往下调时,会用它生成提示,然后显示给用户。

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

ChatWidget::handle_reasoning_shortcut53–120 ↗
fn handle_reasoning_shortcut(&mut self, key_event: KeyEvent) -> bool

作用:这是这个文件的主入口,负责判断一次键盘按键是不是“调推理强度”的快捷键,并真的完成调整。它保证只有在合适的时机才处理,避免和弹窗、启动流程、模型限制打架。

数据流:进去的是一个键盘事件 → 它先判断是调低、调高,还是无关按键;再检查有没有弹窗、会话是否准备好、当前模型有没有可用预设;接着取出模型支持的档位,算出下一档 → 出来的是 true 或 false,表示这个按键有没有被它处理;如果处理成功,还会更新当前推理强度,或在不能调整时显示提示消息。

调用关系:它通常会在聊天界面的普通按键分发之前被调用。它把查模型预设的活交给 ChatWidget::current_model_preset,把整理可选档位的活交给 reasoning_choices,把“下一档是哪一档”的判断交给 next_reasoning_effort。若处在 Plan 模式,它发送 UpdatePlanModeReasoningEffort 事件;否则直接应用临时模型和推理强度设置。

调用图:调用 3 个内部函数(current_model_preset, next_reasoning_effort, reasoning_choices);外部调用 2 个(UpdatePlanModeReasoningEffort, format!)。

ChatWidget::current_model_preset122–129 ↗
fn current_model_preset(&self) -> Option<ModelPreset>

作用:这个函数用来找到当前正在使用的模型对应的预设信息。预设可以理解成“这个模型的说明卡”,里面写着默认推理强度和支持哪些档位。

数据流:它读取当前模型名和模型目录里的模型列表 → 在列表中寻找名字相同的模型预设 → 找到就返回这张预设卡,找不到或列表读取失败就返回空。

调用关系:它只被 ChatWidget::handle_reasoning_shortcut 调用。快捷键要调整推理强度前,必须先知道当前模型允许怎么调,所以这一步相当于先查说明书。

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

reasoning_choices132–142 ↗
fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig>

作用:这个函数把某个模型支持的推理强度档位整理成一个列表。它保证即使模型没有明确列出支持项,也至少有一个默认档位可用。

数据流:进去的是模型预设 → 它从预设里取出所有公开支持的推理强度;如果列表为空,就把默认推理强度放进去 → 出来的是一个按模型声明顺序排列的档位列表。

调用关系:它被 ChatWidget::handle_reasoning_shortcut 调用,用来准备 next_reasoning_effort 需要的“可走楼梯”。后者只负责按这个列表往前或往后移动一格。

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

next_reasoning_effort144–161 ↗
fn next_reasoning_effort(
    choices: &[ReasoningEffortConfig],
    current_effort: Option<ReasoningEffortConfig>,
    direction: ReasoningShortcutDirection,
) -> Option<ReasoningEffortConfig>

作用:这个函数负责计算“当前推理强度往上或往下调一格后是什么”。它不自己决定哪些档位合法,只严格按传进来的列表走。

数据流:进去的是可选档位列表、当前档位、调整方向 → 它先确认当前档位确实在列表里,再根据方向取前一个或后一个 → 出来的是新的档位;如果当前档位不存在、已经到边界、或没有当前档位,就返回空。

调用关系:它是快捷键调整的核心小齿轮,由 ChatWidget::handle_reasoning_shortcut 调用。文件里的多组测试也主要围绕它,确保它在升档、降档、边界、自定义档位等情况下都不出错。

调用图:被 1 处调用(handle_reasoning_shortcut);外部调用 2 个(get, iter)。

tests::next_reasoning_effort_raises_from_default_anchor169–185 ↗
fn next_reasoning_effort_raises_from_default_anchor()

作用:这个测试确认:当前在中等档位时,按“调高”应该变成高档位。它验证最常见的升档行为。

数据流:进去的是低、中、高、超高这几个档位,以及当前中等、方向调高 → 测试调用 next_reasoning_effort → 期望出来的是高档位。

调用关系:它直接检查 next_reasoning_effort 的基本升档规则,帮助保证 ChatWidget::handle_reasoning_shortcut 以后调用它时不会把用户调到错误档位。

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

tests::next_reasoning_effort_lowers_from_default_anchor188–203 ↗
fn next_reasoning_effort_lowers_from_default_anchor()

作用:这个测试确认:当前在中等档位时,按“调低”应该变成低档位。它验证最常见的降档行为。

数据流:进去的是低、中、高三个档位,以及当前中等、方向调低 → 测试调用 next_reasoning_effort → 期望出来的是低档位。

调用关系:它直接保护 next_reasoning_effort 的基本降档规则。这样主快捷键流程在用户按调低键时,才会得到正确结果。

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

tests::next_reasoning_effort_does_not_infer_position_for_unsupported_current206–224 ↗
fn next_reasoning_effort_does_not_infer_position_for_unsupported_current()

作用:这个测试确认:如果当前档位不在模型支持列表里,函数不会自作聪明地猜它应该在哪个位置。这样可以避免调到模型不支持或不符合声明的档位。

数据流:进去的是只支持低和高的列表,但当前档位设成中等 → 它分别尝试调高和调低 → 两次结果都应该是空,表示不能直接移动。

调用关系:它约束 next_reasoning_effort 的安全边界。真正的兜底选择由 ChatWidget::handle_reasoning_shortcut 在调用它之前完成,而不是让这个函数猜。

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

tests::next_reasoning_effort_uses_advertised_order_for_custom_levels227–253 ↗
fn next_reasoning_effort_uses_advertised_order_for_custom_levels()

作用:这个测试确认:如果模型给出的档位顺序很特别,甚至包含自定义档位,函数也要按模型声明的顺序走,而不是按程序员心里的低中高顺序走。

数据流:进去的是一个顺序为高、低、自定义 max 的列表 → 它测试从高调高会到低,以及从自定义 max 调低会到低 → 出来的结果必须符合这个给定顺序。

调用关系:它保护 next_reasoning_effort 对“模型自己公布的顺序”的尊重。这样 ChatWidget::handle_reasoning_shortcut 面对特殊模型时,也不会把顺序改错。

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

tests::next_reasoning_effort_clamps_at_bounds256–279 ↗
fn next_reasoning_effort_clamps_at_bounds()

作用:这个测试确认:已经在最低档时不能再降,已经在最高档时不能再升。也就是调节不会越界。

数据流:进去的是低、中、高三个档位 → 它分别测试低档继续调低、高档继续调高 → 两次结果都应该是空,表示没有下一档。

调用关系:它验证 next_reasoning_effort 的边界行为。主流程拿到空结果后,会用 ReasoningShortcutDirection::bound_message 给用户显示“已经到头了”的提示。

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

tests::next_reasoning_effort_single_option_is_noop282–301 ↗
fn next_reasoning_effort_single_option_is_noop()

作用:这个测试确认:如果模型只有一个推理强度选项,不管调高还是调低都不应该发生变化。

数据流:进去的是只有高档一个选项的列表 → 它尝试从高档调高和调低 → 两次都应该返回空,表示没有可切换的档位。

调用关系:它补上 next_reasoning_effort 的单选项场景。这样当某个模型只支持一个固定档位时,快捷键流程会稳定地给提示,而不是误以为可以调整。

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

tui/src/chatwidget/tokens.rs源码 ↗
orchestration聊天界面运行期间,用户触发 /usage、后台结果返回、以及把结果插入历史记录时活跃

用户输入 /usage 后,界面不能傻等网络结果,也不能反复改已经写进聊天记录的内容。这个文件就像给这张“用量卡片”安排了一个临时等候区:先生成一张包含命令和“Loading...”的卡片,显示在输入框上方;后台请求回来后,再把同一张卡片改成统计图或“不可用”。它用请求编号防止旧请求误改新卡片,比如用户连续点了两次 /usage,第一次的迟到结果不会覆盖第二次。卡片状态放在共享状态里,Arc 是“多人共用同一份东西”的指针,RwLock 是“读写锁”,防止同时读写把状态弄乱。最后,文件还会判断当前聊天流、合并输出、活动单元格等是否还没结束;如果没结束,就先别把用量卡插入历史,避免顺序错乱。

函数细节15
TokenActivityHandle::finish74–76 ↗
fn finish(&self, result: Result<GetAccountTokenUsageResponse, String>)

作用:把后台拿到的用量结果写回那张正在加载的卡片。调用者不用自己提供日期,它会用当前日期来判断统计内容该怎么显示。

数据流:进去的是一次后台请求的结果:要么是成功拿到的用量数据,要么是一段错误信息。它先读取当前日期,再把结果交给 finish_with_today。出来的不是返回值,而是共享卡片状态被改掉:成功就变成“已加载”,失败就变成“出错”。

调用关系:这是后台请求完成后最常用的入口。它自己只做一件事:取当前时间,然后把真正改状态的工作交给 TokenActivityHandle::finish_with_today

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

TokenActivityHandle::finish_with_today78–90 ↗
fn finish_with_today(
        &self,
        result: Result<GetAccountTokenUsageResponse, String>,
        today: NaiveDate,
    )

作用:这是实际修改卡片状态的地方。它把“加载中”换成“有结果”或“不可用”。

数据流:进去的是后台结果和一个明确的“今天是哪一天”。它根据结果成功或失败做出新状态,然后拿到写锁,也就是临时独占这份状态,最后把共享状态替换掉。出来时,所有拿着同一张卡片状态的人都会看到新状态。

调用关系:它被 TokenActivityHandle::finish 调用。拆出这个函数的好处是测试或内部逻辑可以指定日期,而不是总依赖真实当前时间。

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

new_token_activity_output105–122 ↗
fn new_token_activity_output(
    view: TokenActivityView,
) -> (CompositeHistoryCell, TokenActivityHandle)

作用:新建一张 /usage 用量卡片,以及一个之后用来填充结果的“句柄”。句柄可以理解成这张卡片的遥控器。

数据流:进去的是要看的用量视图,比如某种统计范围。它先做一行彩色的 /usage ... 命令回显,再建一个初始为“Loading”的共享状态,然后把命令行和用量卡组合成一张复合历史单元。出来的是这张复合卡片,以及能在后台请求回来后更新它的 TokenActivityHandle

调用关系:它被 ChatWidget::add_token_activity_output 调用。也就是说,每次聊天界面开始处理一次新的 /usage,都会先通过这里造出临时卡片和后续更新用的句柄。

调用图:调用 2 个内部函数(new, new);被 1 处调用(add_token_activity_output);外部调用 4 个(clone, new, new, vec!)。

TokenActivityHistoryCell::display_lines125–143 ↗
fn display_lines(&self, width: u16) -> Vec<Line<'static>>

作用:把用量卡片转换成屏幕上能显示的文字行。它会根据当前状态显示“加载中”“不可用”,或者真正的用量图表。

数据流:进去的是可用宽度。它读取共享状态:如果还在加载,就输出标题和 Loading;如果出错,就输出标题和不可用提示;如果已拿到数据,就把数据、日期和宽度交给图表代码生成显示行。出来的是一组可渲染到终端界面的文字行。

调用关系:这是卡片被界面绘制时会走的核心函数。它在成功状态下把具体画图工作交给 chart::loaded_lines,而 TokenActivityHistoryCell::raw_lines 也会先调用它再转成纯文本。

调用图:调用 1 个内部函数(loaded_lines);被 1 处调用(raw_lines);外部调用 1 个(vec!)。

TokenActivityHistoryCell::raw_lines145–147 ↗
fn raw_lines(&self) -> Vec<Line<'static>>

作用:把用量卡片变成不带复杂样式的纯文本行。这样在需要复制、保存或按普通文本处理时也能使用同一张卡片内容。

数据流:进去没有额外业务数据。它先用最大宽度调用 display_lines 得到完整显示内容,再用 plain_lines 去掉或简化样式。出来的是纯文本形式的行。

调用关系:它依赖 TokenActivityHistoryCell::display_lines 生成内容,再交给 plain_lines 做样式清理。它是同一张卡片在“非界面渲染场景”下的出口。

调用图:调用 2 个内部函数(display_lines, plain_lines)。

ChatWidget::add_token_activity_output156–171 ↗
fn add_token_activity_output(&mut self, view: TokenActivityView)

作用:在聊天界面里开始一次新的 /usage 刷新。它会放上一张临时加载卡,并发出事件让后台去取数据。

数据流:进去的是用户想看的用量视图。它取当前请求编号,然后编号加一;接着调用 new_token_activity_output 生成卡片和句柄;再清掉旧的已完成卡片,把新的卡片放进“正在刷新”的位置;最后要求界面重画,并发送刷新用量的应用事件。出来时,用户能看到加载卡,后台也知道要开始请求。

调用关系:这是 /usage 命令进入这个文件的起点。它把造卡片的工作交给 new_token_activity_output,之后结果会通过 ChatWidget::finish_token_activity_refresh 回来。

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

ChatWidget::pending_token_activity_output178–187 ↗
fn pending_token_activity_output(&self) -> Option<&dyn HistoryCell>

作用:告诉界面现在有没有一张用量卡需要临时显示在输入框上方。它优先显示还在加载的卡,其次显示已经完成但还没写入历史的卡。

数据流:进去的是聊天组件当前保存的状态。它先看有没有正在刷新的用量卡;如果没有,再看有没有已完成但待插入的卡。出来的是一个只读的历史单元引用,或者没有可显示内容。

调用关系:绘制聊天界面时会用它来拿临时显示内容。它不转移卡片所有权,所以后台结果更新和之后插入历史仍然由 ChatWidget 控制。

ChatWidget::finish_token_activity_refresh194–211 ↗
fn finish_token_activity_refresh(
        &mut self,
        request_id: u64,
        result: Result<GetAccountTokenUsageResponse, String>,
    ) -> bool

作用:后台用量请求回来后,用这个函数把结果应用到对应的临时卡片上。它会检查请求编号,避免过期结果乱改界面。

数据流:进去的是请求编号和请求结果。它先取出当前正在刷新的卡;如果没有卡,直接表示没处理;如果编号不匹配,就把卡放回去并表示没处理;如果匹配,就用卡片句柄写入结果,再把卡片移到“已完成、待插入历史”的位置,并要求重画。出来的是 true 或 false,表示这次结果有没有被接收。

调用关系:它接在 ChatWidget::add_token_activity_output 发起的后台请求之后。匹配成功时,它会调用 TokenActivityHandle::finish 更新共享状态;之后卡片通常会等待插入历史。

ChatWidget::usage_history_insertion_blocked218–224 ↗
fn usage_history_insertion_blocked(&self) -> bool

作用:判断现在能不能把已完成的用量卡写进聊天历史。它的重点是避免卡片插队,导致显示顺序和真实发生顺序不一致。

数据流:进去的是聊天组件当前状态。它检查是否还有普通输出流、计划输出流、等待合并的输出、正在活动的历史单元或钩子单元。出来的是一个布尔值:true 表示现在先别插入,false 表示可以插入。

调用关系:准备提交用量卡到历史前会先问它。如果它说被阻塞,调用方应等流结束或合并完成后再试。

ChatWidget::note_stream_consolidation_queued230–233 ↗
fn note_stream_consolidation_queued(&mut self)

作用:记录有一个“流式输出合并”任务排队了。这里的流式输出合并,可以理解成把一段边生成边显示的内容整理成最终历史记录。

数据流:进去的是聊天组件本身。它把待合并计数加一,并且用饱和加法,意思是尽量避免数字溢出造成怪问题。出来时,组件知道还有一个合并屏障没解除。

调用关系:当系统安排一次流式输出合并时调用它。它和 ChatWidget::note_stream_consolidation_completed 成对使用,共同影响 ChatWidget::usage_history_insertion_blocked 的判断。

ChatWidget::note_stream_consolidation_completed239–242 ↗
fn note_stream_consolidation_completed(&mut self)

作用:记录一个流式输出合并任务已经完成。完成后,用量卡可能就可以插入历史了。

数据流:进去的是聊天组件本身。它把待合并计数减一,并且用饱和减法,意思是即使多减一次也不会变成负数。出来时,阻止用量卡插入的屏障少了一个。

调用关系:它通常对应之前的 ChatWidget::note_stream_consolidation_queued。计数变少后,外层流程可能会再次尝试提交等待中的用量输出。

ChatWidget::take_completed_token_activity_output249–253 ↗
fn take_completed_token_activity_output(&mut self) -> Option<CompositeHistoryCell>

作用:把已经完成的用量卡从临时区域拿出来,交给历史记录插入流程。拿走后,它就不再显示在临时位置。

数据流:进去的是聊天组件当前状态。它尝试取出“已完成、待插入”的卡片;如果没有,就返回空;如果有,就清掉这个待插入槽位,并更新活动单元版本。出来的是那张可以写入历史的复合卡片。

调用关系:调用方应该先用 ChatWidget::usage_history_insertion_blocked 确认没有阻塞,再调用它。它是用量卡从临时显示走向正式聊天历史的交接点。

ChatWidget::request_pending_usage_output_insertion259–265 ↗
fn request_pending_usage_output_insertion(&self)

作用:如果有用量相关内容正在等着写入历史,就发一个事件,让主流程稍后尝试提交。它像是提醒系统:“别忘了这里还有东西没落座。”

数据流:进去的是聊天组件当前状态。它检查有没有已完成的用量卡,或者有没有等待显示的限流重置提示;如果有,就发送 CommitPendingUsageOutput 事件。出来时没有直接返回内容,但事件队列里可能多了一个提交请求。

调用关系:它通常在某些阻塞条件可能刚刚解除后使用,比如输出合并或历史状态变化后。真正拿走卡片的工作会在之后的提交流程里发生。

ChatWidget::request_pending_usage_output_insertion_after_stream_shutdown267–274 ↗
fn request_pending_usage_output_insertion_after_stream_shutdown(&self)

作用:在输出流关闭后,如果还有用量相关内容等待写入历史,就发一个专门的“流关闭后再提交”事件。

数据流:进去的是聊天组件当前状态。它检查有没有已完成用量卡或限流重置提示;如果有,就发送 CommitPendingUsageOutputAfterStreamShutdown 事件。出来时没有直接结果,但主事件循环会获得一次稍后提交的机会。

调用关系:它和 ChatWidget::request_pending_usage_output_insertion 很像,但用于流刚关闭这种更具体的时机。这样可以避免在流还没彻底收尾时插入用量卡。

ChatWidget::clear_pending_token_activity_refreshes280–287 ↗
fn clear_pending_token_activity_refreshes(&mut self)

作用:清掉所有还没落进历史的用量卡,包括正在加载的和已经完成但没插入的。这样在重置聊天、回退历史或替换内容时,旧结果不会再影响新界面。

数据流:进去的是聊天组件当前状态。它移除正在刷新的卡,也移除已完成待插入的卡;如果确实清掉了东西,就更新活动单元版本并要求重画。出来时,旧的用量刷新状态不再由组件持有。

调用关系:它用于聊天记录发生大变化的时候。这样即使后台旧请求晚点返回,也无法把已经不该存在的卡片重新塞回界面。

tui/src/chatwidget/usage.rs源码 ↗
orchestrationstartup, user interaction, request handling

这个文件解决的是一个很具体的界面问题:用户想知道自己最近用了多少额度,或者想把当前的限额窗口重置一下。它像一个前台服务员:先摆出“Usage”菜单;如果用户要看重置次数,就显示“正在查询”;服务器回结果后,再换成“可以使用”“没有次数”“出错了”等不同弹窗。这里还用了请求编号,意思是每次查询或使用重置次数都贴一个小标签,回来的结果必须对得上标签才算数,这样旧请求的结果不会误改新界面。使用重置次数时还会生成一个幂等键(一个唯一字符串,用来避免重复点击造成重复扣次数)。启动时它也能悄悄查一次,如果发现用户有可用重置,就在聊天记录里放一条提示。

函数细节19
ChatWidget::open_usage_menu12–57 ↗
fn open_usage_menu(&mut self)

作用:打开“Usage”菜单,让用户选择查看 token 用量,或者兑换一次限额重置。它会根据账号类型和已知的可用次数,决定“兑换重置”这一项能不能点。

数据流:进去的是当前聊天窗口里的账号信息、套餐类型、已缓存的可用重置次数 → 它先清掉旧的重置提示,再组装一个底部弹窗菜单 → 出来的是界面上出现“Show usage”和“Redeem rate limit reset”两个选项,并触发重画屏幕。

调用关系:这是用户主动打开 /usage 一类入口时的第一站。它先调用 ChatWidget::clear_pending_rate_limit_reset_hint 清理旧提示;用户选菜单项后,菜单动作会发出后续事件,让别的流程去打开用量页或查询重置次数。

调用图:调用 1 个内部函数(clear_pending_rate_limit_reset_hint);外部调用 3 个(default, format!, vec!)。

ChatWidget::show_rate_limit_reset_loading_popup59–76 ↗
fn show_rate_limit_reset_loading_popup(&mut self) -> u64

作用:在查询用户还有多少限额重置次数时,先显示一个“正在加载”的弹窗。这样用户知道系统已经开始查了,而不是卡住没反应。

数据流:进去的是当前界面状态 → 它清掉旧提示,生成一个新的请求编号,把这个编号记为正在等待的查询,再显示“Checking your available resets...” → 出来的是一个请求编号,调用者拿它去匹配稍后回来的查询结果。

调用关系:它通常在用户选择“Redeem rate limit reset”之后使用。它会调用 ChatWidget::clear_pending_rate_limit_reset_hint 和 ChatWidget::take_next_rate_limit_reset_request_id;后面服务器结果回来时,会交给 ChatWidget::finish_rate_limit_reset_credits_refresh。

调用图:调用 2 个内部函数(clear_pending_rate_limit_reset_hint, take_next_rate_limit_reset_request_id);外部调用 2 个(default, vec!)。

ChatWidget::finish_rate_limit_reset_credits_refresh78–110 ↗
fn finish_rate_limit_reset_credits_refresh(
        &mut self,
        request_id: u64,
        result: Result<RateLimitResetCreditsSummary, String>,
    ) -> bool

作用:处理“查询可用重置次数”的结果,并把加载弹窗换成真正的结果弹窗。它也负责丢掉过期请求的结果,避免旧结果覆盖新界面。

数据流:进去的是请求编号,以及成功或失败的查询结果 → 它先检查编号是不是当前等待的那个;如果不是就什么都不改;如果是,就保存可用次数,并根据结果生成确认使用、没有次数或加载失败的弹窗 → 出来的是是否真的更新了当前弹窗。

调用关系:它接在 ChatWidget::show_rate_limit_reset_loading_popup 之后。成功且有次数时交给 ChatWidget::rate_limit_reset_confirmation_params 做确认弹窗;没有次数或失败时交给 ChatWidget::rate_limit_reset_message_params 做普通消息弹窗。

调用图:外部调用 2 个(rate_limit_reset_confirmation_params, rate_limit_reset_message_params)。

ChatWidget::rate_limit_reset_confirmation_params112–143 ↗
fn rate_limit_reset_confirmation_params(available_count: i64) -> SelectionViewParams

作用:生成“你有可用重置次数,要不要用一次?”这个确认弹窗的配置。它默认选中“Cancel”,避免用户手滑直接消耗次数。

数据流:进去的是可用重置次数 → 它生成一个唯一的幂等键,并拼出标题、副标题、按钮和按钮动作 → 出来的是一份弹窗参数;如果用户点“Use a reset”,会发送带幂等键的消耗重置次数事件。

调用关系:它由 ChatWidget::finish_rate_limit_reset_credits_refresh 在发现用户有可用次数时调用。它还会用 reset_label 形成单复数正确的提示文案,并把真正消耗次数的活儿交给后续事件流程。

调用图:外部调用 4 个(default, new_v4, format!, vec!)。

ChatWidget::rate_limit_reset_message_params145–157 ↗
fn rate_limit_reset_message_params(message: &str) -> SelectionViewParams

作用:生成一个简单的信息弹窗,只显示一句说明和一个“Close”按钮。它用于没有次数、失败、无需重置等不需要复杂选择的情况。

数据流:进去的是要展示给用户的一句话 → 它把这句话放进固定标题的弹窗里,并添加关闭按钮 → 出来的是可直接显示或替换当前弹窗的参数。

调用关系:它是多个流程共用的小帮手。ChatWidget::finish_rate_limit_reset_credits_refresh、ChatWidget::finish_rate_limit_reset_consume 和 ChatWidget::finish_post_consume_reset_credits_refresh 都会用它来显示最终说明。

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

ChatWidget::show_rate_limit_reset_consuming_popup159–177 ↗
fn show_rate_limit_reset_consuming_popup(&mut self) -> u64

作用:在用户确认使用一次限额重置后,显示“正在重置你的用量”的弹窗。这个弹窗不允许取消,避免用户误以为中途取消就能撤回已经发出的操作。

数据流:进去的是当前界面状态 → 它清掉旧提示,取一个新请求编号,记录为正在等待的消耗请求,并显示“Resetting your usage...” → 出来的是请求编号,供后续消耗结果回来时核对。

调用关系:它在真正发送“使用一次重置”的请求前后出现。它调用 ChatWidget::clear_pending_rate_limit_reset_hint 和 ChatWidget::take_next_rate_limit_reset_request_id;结果回来后由 ChatWidget::finish_rate_limit_reset_consume 接着处理。

调用图:调用 2 个内部函数(clear_pending_rate_limit_reset_hint, take_next_rate_limit_reset_request_id);外部调用 2 个(default, vec!)。

ChatWidget::finish_rate_limit_reset_consume179–245 ↗
fn finish_rate_limit_reset_consume(
        &mut self,
        request_id: u64,
        idempotency_key: String,
        result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
    ) -> bo

作用:处理“使用一次限额重置”的结果。它会区分成功、已经兑换过、没必要重置、没有次数、网络或服务器失败,并显示不同的下一步。

数据流:进去的是请求编号、幂等键,以及成功或失败的消耗结果 → 它先确认请求编号没过期;成功重置或已兑换时,清空已知次数并显示“正在刷新剩余次数”;没有次数或无需重置时显示说明;失败时显示“Try again”和“Close” → 出来的是一个布尔值,表示是否进入了成功后的刷新流程,同时界面弹窗被替换。

调用关系:它接在 ChatWidget::show_rate_limit_reset_consuming_popup 之后。它用 ChatWidget::replace_rate_limit_reset_popup 更新弹窗;成功后会用 ChatWidget::rate_limit_reset_success_loading_params,失败或普通结果会用 ChatWidget::rate_limit_reset_message_params 或自己构造重试弹窗。

调用图:调用 1 个内部函数(replace_rate_limit_reset_popup);外部调用 6 个(default, rate_limit_reset_message_params, rate_limit_reset_success_loading_params, matches!, unreachable!, vec!)。

ChatWidget::finish_post_consume_reset_credits_refresh247–270 ↗
fn finish_post_consume_reset_credits_refresh(
        &mut self,
        request_id: u64,
        result: Result<RateLimitResetCreditsSummary, String>,
    ) -> bool

作用:在成功使用一次重置后,再处理“刷新剩余重置次数”的结果。它把最终结果告诉用户:已经重置,并尽量说明还剩几次。

数据流:进去的是请求编号,以及刷新剩余次数的结果 → 它确认编号正确,结束等待状态;如果刷新成功,就保存新的可用次数并写出“还剩几次”;如果刷新失败,也至少告诉用户“Usage reset.” → 出来的是最终消息弹窗,并返回是否处理成功。

调用关系:它接在 ChatWidget::finish_rate_limit_reset_consume 的成功路径之后。它通过 ChatWidget::rate_limit_reset_message_params 生成最终消息,再用 ChatWidget::replace_rate_limit_reset_popup 替换正在刷新的弹窗。

调用图:调用 1 个内部函数(replace_rate_limit_reset_popup);外部调用 2 个(rate_limit_reset_message_params, format!)。

ChatWidget::rate_limit_reset_success_loading_params272–285 ↗
fn rate_limit_reset_success_loading_params() -> SelectionViewParams

作用:生成“重置已成功,正在检查剩余次数”的加载弹窗。它用于成功重置之后、最终剩余次数回来之前的短暂阶段。

数据流:进去不需要额外输入 → 它组装一个固定标题、固定说明、禁用的“Refreshing...”项目,并禁止取消 → 出来的是一份弹窗参数。

调用关系:它由 ChatWidget::finish_rate_limit_reset_consume 在重置成功或已经兑换过时调用。之后通常会等待一次新的可用次数查询,再由 ChatWidget::finish_post_consume_reset_credits_refresh 收尾。

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

ChatWidget::replace_rate_limit_reset_popup287–294 ↗
fn replace_rate_limit_reset_popup(&mut self, params: SelectionViewParams)

作用:把当前的限额重置弹窗换成新的内容,并在真的换成功时要求界面重画。它避免到处重复写同一段“替换弹窗再刷新”的代码。

数据流:进去的是一份新的弹窗参数 → 它只在当前确实存在限额重置弹窗时替换内容 → 出来的是界面被更新;如果没有这个弹窗,就不做事。

调用关系:它是消耗流程里的通用换屏工具。ChatWidget::finish_rate_limit_reset_consume 和 ChatWidget::finish_post_consume_reset_credits_refresh 都把更新弹窗的细活交给它。

调用图:被 2 处调用(finish_post_consume_reset_credits_refresh, finish_rate_limit_reset_consume)。

ChatWidget::start_rate_limit_reset_startup_check296–301 ↗
fn start_rate_limit_reset_startup_check(&mut self) -> u64

作用:启动时发起一次后台检查,看看用户有没有可用的限额重置次数。它不会立刻弹窗打扰用户,只是准备接收结果。

数据流:进去的是当前聊天窗口状态 → 它清掉旧提示,生成一个新的请求编号,并把它记为“启动提示检查”的等待编号 → 出来的是请求编号,外部拿它去查询服务器并在回来时匹配。

调用关系:它通常在程序启动或账号状态准备好后调用。它依赖 ChatWidget::clear_pending_rate_limit_reset_hint 清理旧状态,并用 ChatWidget::take_next_rate_limit_reset_request_id 生成编号;结果回来交给 ChatWidget::finish_rate_limit_reset_hint_refresh。

调用图:调用 2 个内部函数(clear_pending_rate_limit_reset_hint, take_next_rate_limit_reset_request_id)。

ChatWidget::finish_rate_limit_reset_hint_refresh303–323 ↗
fn finish_rate_limit_reset_hint_refresh(
        &mut self,
        request_id: u64,
        result: Result<RateLimitResetCreditsSummary, String>,
    ) -> bool

作用:处理启动时后台检查到的重置次数,并决定要不要在聊天里放一条提示。它不会给工作区账号提示,因为这类账号不适用这里的重置。

数据流:进去的是请求编号,以及查询结果 → 它先确认编号是否匹配;再检查是否还有后端登录、是否是工作区账号;如果结果成功且有次数,就保存次数并设置一条提示 → 出来的是是否处理了这次结果,可能还会新增一条聊天提示。

调用关系:它接在 ChatWidget::start_rate_limit_reset_startup_check 之后。真正创建提示的工作交给 ChatWidget::set_rate_limit_reset_available_hint。

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

ChatWidget::clear_pending_rate_limit_reset_requests325–331 ↗
fn clear_pending_rate_limit_reset_requests(&mut self)

作用:一次性清掉所有和限额重置相关的等待状态、缓存次数和弹窗。它适合在账号变化、退出登录或需要重置界面状态时使用。

数据流:进去的是当前组件里的重置相关状态 → 它清空正在等待的请求编号和已知可用次数,清掉提示,并关闭限额重置弹窗 → 出来的是界面回到没有重置流程挂起的干净状态。

调用关系:它调用 ChatWidget::clear_pending_rate_limit_reset_hint 清理提示部分,自己再负责请求状态、缓存次数和弹窗。它是这个功能的“大扫除”入口。

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

ChatWidget::clear_pending_rate_limit_reset_hint333–340 ↗
fn clear_pending_rate_limit_reset_hint(&mut self)

作用:清掉那条“你有可用限额重置”的待显示提示。如果提示真的被清掉了,它会通知界面更新。

数据流:进去的是当前可能存在的待显示提示 → 它取消提示检查编号,并尝试拿走提示内容;如果确实有提示,就更新聊天单元版本并请求重画 → 出来的是提示不再等待显示。

调用关系:很多新流程开始前都会先调用它,避免旧提示和新弹窗同时出现。ChatWidget::open_usage_menu、ChatWidget::show_rate_limit_reset_loading_popup、ChatWidget::show_rate_limit_reset_consuming_popup、ChatWidget::start_rate_limit_reset_startup_check 和 ChatWidget::clear_pending_rate_limit_reset_requests 都会用到它。

调用图:被 5 处调用(clear_pending_rate_limit_reset_requests, open_usage_menu, show_rate_limit_reset_consuming_popup, show_rate_limit_reset_loading_popup, start_rate_limit_reset_startup_check)。

ChatWidget::pending_rate_limit_reset_hint342–344 ↗
fn pending_rate_limit_reset_hint(&self) -> Option<&PlainHistoryCell>

作用:查看当前有没有一条待显示的限额重置提示,但不把它拿走。它适合只是想读一下状态的地方使用。

数据流:进去的是当前聊天组件 → 它只查看内部保存的提示引用 → 出来的是“有提示就给出提示内容,没有就给空”,不会修改任何状态。

调用关系:它是只读入口,供界面或渲染相关代码判断是否需要展示提示。和 ChatWidget::take_pending_rate_limit_reset_hint 不同,它不会消费掉提示。

ChatWidget::take_pending_rate_limit_reset_hint346–350 ↗
fn take_pending_rate_limit_reset_hint(&mut self) -> Option<PlainHistoryCell>

作用:取走当前待显示的限额重置提示。取走后,这条提示就不再留在待处理状态里。

数据流:进去的是当前可能保存的提示 → 如果没有提示,就返回空;如果有,就把提示从组件里移出,并更新聊天单元版本 → 出来的是那条提示内容,内部状态变成没有这条待显示提示。

调用关系:它通常给真正要把提示放进聊天历史的地方使用。它和 ChatWidget::pending_rate_limit_reset_hint 配成一对:一个只看,一个取走。

ChatWidget::set_rate_limit_reset_available_hint352–365 ↗
fn set_rate_limit_reset_available_hint(&mut self, available_count: i64)

作用:当发现用户有可用限额重置次数时,创建一条友好的聊天提示,告诉用户可以运行 /usage 来使用。没有次数时它什么都不做。

数据流:进去的是可用次数 → 如果次数小于等于 0,直接结束;如果大于 0,就生成一条信息事件,写入待显示提示,并要求界面刷新 → 出来的是聊天界面将能看到一条关于可用重置次数的提示。

调用关系:它由 ChatWidget::finish_rate_limit_reset_hint_refresh 在启动后台检查成功后调用。它还会借助 reset_label 让英文提示里的单数和复数说法正确。

调用图:被 1 处调用(finish_rate_limit_reset_hint_refresh);外部调用 2 个(format!, new_info_event)。

ChatWidget::take_next_rate_limit_reset_request_id367–373 ↗
fn take_next_rate_limit_reset_request_id(&mut self) -> u64

作用:生成下一个限额重置相关请求编号。这个编号像取号机的小票,用来确认稍后回来的结果是不是当前这次请求的。

数据流:进去的是组件内部保存的下一个编号 → 它把当前编号取出来,然后把内部编号加一;如果数字大到溢出,就按无符号整数规则绕回去 → 出来的是本次请求要用的编号。

调用关系:所有需要等待服务器结果的重置流程都会用它。ChatWidget::show_rate_limit_reset_loading_popup、ChatWidget::show_rate_limit_reset_consuming_popup 和 ChatWidget::start_rate_limit_reset_startup_check 都靠它给请求贴标签。

调用图:被 3 处调用(show_rate_limit_reset_consuming_popup, show_rate_limit_reset_loading_popup, start_rate_limit_reset_startup_check)。

reset_label376–382 ↗
fn reset_label(count: i64) -> &'static str

作用:根据数量返回英文里的单数或复数说法。这样界面不会出现“1 rate-limit resets”这种别扭文案。

数据流:进去的是一个数量 → 它判断数量是否等于 1 → 出来的是“rate-limit reset”或“rate-limit resets”这两个固定字符串之一。

调用关系:它是文案小工具,被多个提示和弹窗间接使用,用来让可用次数说明读起来更自然。