专用交互流程与辅助 TUI 处理器
这一阶段像主聊天旁边的一排小工具,用户需要临时操作时才接手,不是核心干活循环。cloud-tasks 三件套负责记住任务、新建任务并画出任务界面。回溯和全屏查看器让人翻旧记录、退回重改。keymap 几个文件提供改快捷键、查按键、保存配置。多智能体导航管切换和状态展示。pets 负责选宠物、预览、加载和出错兜底。剪贴板、主题选择、外部配置导入和平台小检查,则把零散入口收拾成顺手的界面流程。
云任务 UI 状态
这些文件定义 cloud-tasks TUI 状态,初始化新任务 composer,并渲染面向任务的界面和覆盖层。
cloud-tasks/src/app.rs源码 ↗
这个文件主要服务一个 TUI,也就是“终端里的图形界面”。它把用户能看到和操作的东西都放进几个状态对象里:任务列表、环境筛选、差异查看弹层、新建任务页、应用补丁弹窗等。App 像一个前台柜台,记住当前排队的任务和光标位置;DiffOverlay 像一个详情抽屉,用来在“提示文本”和“代码改动 diff”之间切换;AppEvent 则像后台工作人员递来的纸条,告诉界面“任务加载好了”“详情失败了”“应用补丁完成了”。这里还提供 load_tasks,从云端后端拉取任务,并过滤掉只用于 review 的任务。文件底部有测试用的 FakeBackend,用假数据检查环境筛选是否真的传给了后端。
App::new78–102 ↗
fn new() -> Self
作用:创建一个全新的应用状态,给终端界面第一次启动时用。它把列表、弹窗、加载标记、默认提示文字等都放到一个安全的初始状态。
数据流:进去没有外部输入 → 它新建空任务列表、把选中位置设为 0、设置状态文字“Press r to refresh”,并把各种弹窗和后台加载标记清空 → 出来一个可以直接交给主界面循环使用的 App。
调用关系:它会被 run_main 在程序启动时调用,相当于先搭好舞台。它内部会创建空集合等基础容器,之后用户刷新、切换任务、打开详情时都会不断改动这个 App。
App::next104–109 ↗
fn next(&mut self)
作用:把任务列表里的选中项往下移一格。用户按“向下”或类似快捷键时会需要它。
数据流:进去的是当前 App,其中包含任务列表和当前选中的序号 → 如果列表为空就什么也不做;否则把 selected 加 1,但不会超过最后一条 → 出来是同一个 App,只是选中位置可能变了。
调用关系:它属于界面导航的小动作。调用图没有显示直接调用者,但通常会由键盘事件处理代码在用户想选下一条任务时调用。
App::prev111–118 ↗
fn prev(&mut self)
作用:把任务列表里的选中项往上移一格。用户按“向上”或类似快捷键时会用到它。
数据流:进去的是当前 App → 如果列表为空就不动;如果当前不是第一条,就把 selected 减 1 → 出来是同一个 App,选中位置最多往前移动一格。
调用关系:它和 App::next 配成一对,负责列表上下移动。调用图没有显示直接调用者,但它通常会被主界面的按键处理部分使用。
load_tasks121–134 ↗
async fn load_tasks(
backend: &dyn CloudBackend,
env: Option<&str>,
) -> anyhow::Result<Vec<TaskSummary>>
作用:从云任务后端拉取任务列表,并按当前环境筛选。它还会过滤掉只用于 review 的任务,让主列表只显示普通任务。
数据流:进去的是一个 CloudBackend(云任务后端接口)和可选的环境 id → 它最多等 5 秒,请后端列出最多 20 个任务,然后丢掉 is_review 为真的任务 → 出来是一个任务摘要列表;如果超时或后端报错,就返回错误。
调用关系:run_main 会在需要刷新列表时调用它;测试 tests::load_tasks_uses_env_parameter 也会调用它验证环境参数没有丢。它把真正联网或取数据的工作交给 backend.list_tasks,并用 timeout 给这次请求加上时间限制,避免界面一直卡住。
调用图:被 2 处调用(load_tasks_uses_env_parameter, run_main);外部调用 3 个(from_secs, list_tasks, timeout)。
AttemptView::has_diff164–166 ↗
fn has_diff(&self) -> bool
作用:判断某一次尝试有没有代码改动内容。界面可以用它决定是否显示 diff 相关入口或提示。
数据流:进去的是一个 AttemptView → 它检查 diff_lines 这个列表是不是空 → 出来是 true 或 false,true 表示有可展示的代码改动行。
调用关系:调用图没有显示直接调用者。它是 AttemptView 上的便捷判断,供详情界面或渲染代码在需要区分“有改动”和“没改动”时使用。
AttemptView::has_text168–170 ↗
fn has_text(&self) -> bool
作用:判断某一次尝试有没有文字输出或原始提示。也就是看它是否有可以在“Prompt/输出”视图里展示的内容。
数据流:进去的是一个 AttemptView → 它检查 text_lines 是否非空,或者 prompt 是否存在 → 出来是布尔值,表示有没有文字内容。
调用关系:调用图没有显示直接调用者。它和 AttemptView::has_diff 类似,是给界面判断展示状态用的小工具。
DiffOverlay::new174–192 ↗
fn new(task_id: TaskId, title: String, attempt_total_hint: Option<usize>) -> Self
作用:创建一个新的任务详情弹层,用来显示某个任务的提示、输出和代码改动。用户打开任务详情时需要这样一个容器。
数据流:进去的是任务 id、标题,以及可能知道的尝试总数 → 它创建一个空的 ScrollableDiff(可滚动的文本区域),放入一个默认尝试,并默认停在 Prompt 视图 → 出来是一个准备好但内容还可能为空的 DiffOverlay。
调用关系:调用图显示它会创建 ScrollableDiff 和向量。它通常由打开详情的流程使用,之后后台加载到的 diff、消息和尝试列表会填进这个弹层。
DiffOverlay::current_attempt194–196 ↗
fn current_attempt(&self) -> Option<&AttemptView>
作用:取出当前选中的那一次尝试。详情页有多次尝试时,很多操作都要先知道用户现在看的是哪一次。
数据流:进去的是 DiffOverlay → 它用 selected_attempt 作为下标去 attempts 列表里找 → 出来是当前 AttemptView 的只读引用;如果下标无效,就返回空。
调用关系:DiffOverlay::apply_selection_to_fields 会用它把当前尝试的内容同步到显示区;DiffOverlay::current_can_apply 会用它判断当前尝试有没有可应用的 diff。
调用图:被 2 处调用(apply_selection_to_fields, current_can_apply)。
DiffOverlay::base_attempt_mut198–203 ↗
fn base_attempt_mut(&mut self) -> &mut AttemptView
作用:拿到第一份尝试,并允许修改它。第一份尝试在这里相当于“基础尝试”,详情刚加载时常先把内容塞到这里。
数据流:进去的是可修改的 DiffOverlay → 如果 attempts 为空,它先补一个默认 AttemptView;然后返回 attempts[0] 的可修改引用 → 出来的是可以写入 diff、文本、提示等内容的基础尝试。
调用关系:调用图没有显示直接调用者。它给外部加载详情的代码一个安全入口,避免列表为空时直接写第一项导致出错。
调用图:外部调用 1 个(default)。
DiffOverlay::set_view205–208 ↗
fn set_view(&mut self, view: DetailView)
作用:切换详情弹层当前看的页面,比如从“提示/输出”切到“代码改动 diff”。
数据流:进去的是目标 DetailView → 它先记录新视图,再调用 apply_selection_to_fields,把当前尝试对应的内容放进可滚动显示区 → 出来是更新后的 DiffOverlay。
调用关系:它把真正刷新显示内容的活儿交给 DiffOverlay::apply_selection_to_fields。用户按键切换详情页标签时,通常会走到这个函数。
调用图:调用 1 个内部函数(apply_selection_to_fields)。
DiffOverlay::expected_attempts210–218 ↗
fn expected_attempts(&self) -> Option<usize>
作用:估算这项任务应该有多少次尝试。它优先用后端给的总数提示;没有提示时,就用当前已经加载到的尝试数量。
数据流:进去的是 DiffOverlay → 它先看 attempt_total_hint;如果没有,再看 attempts 是否为空 → 出来是可选的数量,可能是 Some(数量),也可能是 None。
调用关系:DiffOverlay::attempt_display_total 会调用它,用来决定界面上显示“第几次 / 共几次”时的总数。
调用图:被 1 处调用(attempt_display_total)。
DiffOverlay::attempt_count220–222 ↗
fn attempt_count(&self) -> usize
作用:返回当前已经加载到本地的尝试数量。它回答的是“手里现在有几份”,不是后端可能总共有几份。
数据流:进去的是 DiffOverlay → 它读取 attempts 列表长度 → 出来是一个数字。
调用关系:调用图没有显示直接调用者。它适合给界面或调试代码使用,用来展示或判断当前已加载的尝试数。
DiffOverlay::attempt_display_total224–227 ↗
fn attempt_display_total(&self) -> usize
作用:给界面计算应该显示的尝试总数。它保证即使还没加载到内容,也至少显示成 1,避免出现“共 0 次”这种怪提示。
数据流:进去的是 DiffOverlay → 它调用 expected_attempts;如果拿不到预期总数,就用当前 attempts 长度和 1 里的较大值 → 出来是一个适合展示给用户看的总数。
调用关系:它依赖 DiffOverlay::expected_attempts。通常用于详情页顶部或状态栏,显示当前尝试在总尝试中的位置。
调用图:调用 1 个内部函数(expected_attempts)。
DiffOverlay::step_attempt229–242 ↗
fn step_attempt(&mut self, delta: isize) -> bool
作用:在多次尝试之间前后切换。它支持绕圈:在最后一次再往后会回到第一次。
数据流:进去的是一个步长 delta,比如 1 表示下一次,-1 表示上一次 → 如果尝试数不超过 1,就返回 false;否则算出新的 selected_attempt,并刷新显示内容 → 出来是 true 或 false,同时 DiffOverlay 的当前选择和显示区会被更新。
调用关系:它在切换完下标后调用 DiffOverlay::apply_selection_to_fields,确保屏幕内容跟着变。用户按切换尝试的快捷键时会用到它。
调用图:调用 1 个内部函数(apply_selection_to_fields)。
DiffOverlay::current_can_apply244–251 ↗
fn current_can_apply(&self) -> bool
作用:判断当前看到的内容能不能执行“应用补丁”。只有正在看 diff,而且当前尝试确实有原始 diff 文本时,才算可以应用。
数据流:进去的是 DiffOverlay → 它先确认 current_view 是 Diff,再取 current_attempt,看 diff_raw 是否存在且不为空 → 出来是布尔值,true 表示当前可以尝试应用这份改动。
调用关系:它调用 DiffOverlay::current_attempt。界面可以用它决定是否启用 apply 按钮或快捷键,避免用户在没有补丁内容时误操作。
调用图:调用 1 个内部函数(current_attempt);外部调用 1 个(matches!)。
DiffOverlay::apply_selection_to_fields253–288 ↗
fn apply_selection_to_fields(&mut self)
作用:把“当前选中的尝试”和“当前视图”真正同步到显示字段里。可以把它理解成按下电视遥控器后,屏幕内容跟着换台。
数据流:进去的是可修改的 DiffOverlay → 它读取当前 AttemptView 的 diff、文字和提示,复制到 overlay 的公共字段;如果当前看 Diff,就把 diff 行放进 ScrollableDiff,没有则显示“没有 diff”;如果当前看 Prompt,就放文字输出,没有则显示“没有输出” → 出来是显示区内容已刷新后的 DiffOverlay。
调用关系:DiffOverlay::set_view 和 DiffOverlay::step_attempt 都会调用它,因为切换视图或切换尝试后都必须刷新屏幕。它还会调用 current_attempt 和 ScrollableDiff 的 set_content。
调用图:调用 2 个内部函数(current_attempt, set_content);被 2 处调用(set_view, step_attempt);外部调用 1 个(vec!)。
tests::FakeBackend::list_tasks432–439 ↗
fn list_tasks(
&'a self,
env: Option<&'a str>,
limit: Option<i64>,
cursor: Option<&'a str>,
) -> CloudBackendFuture<'a, codex_cloud_tasks_client
作用:测试里用的假后端列任务函数。它不访问真实网络,而是按传入环境返回预先准备好的任务标题。
数据流:进去的是可选环境 id、数量限制和游标 → 它从 by_env 这张表里找对应标题,组装成 TaskSummary,并按限制最多返回指定数量 → 出来是一个假的 TaskListPage,里面有任务列表和传回的 cursor。
调用关系:tests::FakeBackend::get_task_summary 会调用它来从假列表里找单个任务。测试中的 load_tasks 最终也会通过 CloudBackend 接口走到这个假实现,从而验证环境筛选逻辑。
调用图:被 1 处调用(get_task_summary);外部调用 7 个(pin, now, new, default, new, list_tasks, format!)。
tests::FakeBackend::get_task_summary441–443 ↗
fn get_task_summary(&self, id: TaskId) -> CloudBackendFuture<'_, TaskSummary>
作用:测试里按任务 id 找一条任务摘要。它让假后端看起来更像真实后端。
数据流:进去的是任务 id → 它先调用 tests::FakeBackend::list_tasks 拿默认环境下的假任务,再找 id 相同的那一条 → 找到就返回 TaskSummary,找不到就返回“任务不存在”的错误。
调用关系:它把列任务的工作交给 tests::FakeBackend::list_tasks。调用图没有显示业务测试直接用它,但 CloudBackend 接口要求提供这个能力。
调用图:调用 1 个内部函数(list_tasks);外部调用 2 个(pin, get_task_summary)。
tests::FakeBackend::get_task_diff445–451 ↗
fn get_task_diff(&self, _id: TaskId) -> CloudBackendFuture<'_, Option<String>>
作用:测试假后端里的“取 diff”占位函数。当前测试不需要它,所以明确返回“未实现”。
数据流:进去的是任务 id,但这里不使用 → 它直接构造一个 Unimplemented 错误 → 出来是失败结果。
调用关系:它是为了满足 CloudBackend 接口而存在。当前 load_tasks_uses_env_parameter 测试只关心列表,不会用到 diff。
调用图:外部调用 2 个(pin, Unimplemented)。
tests::FakeBackend::get_task_messages453–455 ↗
fn get_task_messages(&self, _id: TaskId) -> CloudBackendFuture<'_, Vec<String>>
作用:测试假后端里的“取消息列表”函数。为了简单,它总是返回空消息。
数据流:进去的是任务 id,但这里不使用 → 它返回一个空 Vec,也就是空列表 → 出来是成功结果,表示没有消息。
调用关系:它同样是 CloudBackend 接口的一部分。当前测试不依赖它的内容,只需要假后端能完整实现接口。
调用图:外部调用 2 个(pin, vec!)。
tests::FakeBackend::get_task_text457–462 ↗
fn get_task_text(
&self,
id: TaskId,
) -> CloudBackendFuture<'_, codex_cloud_tasks_client::TaskText>
作用:测试假后端里的“取任务文字详情”函数。它返回一份固定的示例提示和尝试状态。
数据流:进去的是任务 id,但这里不按 id 区分 → 它创建 TaskText,包含 prompt、turn_id、attempt_placement 和 Completed 状态 → 出来是固定的文字详情结果。
调用关系:CloudBackend 的 trait 方法会把请求转给这个假实现。当前环境筛选测试主要不用它,但详情相关代码如果在测试中调用,也能拿到合理的假数据。
调用图:外部调用 3 个(pin, new, get_task_text)。
tests::FakeBackend::list_sibling_attempts464–470 ↗
fn list_sibling_attempts(
&self,
_task: TaskId,
_turn_id: String,
) -> CloudBackendFuture<'_, Vec<codex_cloud_tasks_client::TurnAttempt>>
作用:测试假后端里的“列出同一轮的其他尝试”函数。当前不需要多尝试数据,所以返回空列表。
数据流:进去的是任务 id 和 turn_id → 它不查询任何东西,直接返回空 Vec → 出来表示没有兄弟尝试。
调用关系:这是 CloudBackend 接口要求的能力。当前测试没有真正使用它,但有了它,FakeBackend 才能作为完整后端传给通用代码。
调用图:外部调用 2 个(pin, new)。
tests::FakeBackend::apply_task472–482 ↗
fn apply_task(
&self,
_id: TaskId,
_diff_override: Option<String>,
) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome>
作用:测试假后端里的“真正应用补丁”占位函数。因为当前测试不做应用补丁,所以它故意返回未实现错误。
数据流:进去的是任务 id 和可选 diff 覆盖内容 → 它不做任何补丁应用,直接返回 Unimplemented 错误 → 出来是失败结果。
调用关系:它用于补齐 CloudBackend 接口。load_tasks_uses_env_parameter 只测试加载列表,因此不会走到这个函数。
调用图:外部调用 2 个(pin, Unimplemented)。
tests::FakeBackend::apply_task_preflight484–494 ↗
fn apply_task_preflight(
&self,
_id: TaskId,
_diff_override: Option<String>,
) -> CloudBackendFuture<'_, codex_cloud_tasks_client::ApplyOutcome>
作用:测试假后端里的“应用前检查”占位函数。应用前检查就是先看看补丁能不能安全应用,但这里不实际支持。
数据流:进去的是任务 id 和可选 diff 覆盖内容 → 它直接返回 Unimplemented 错误 → 出来是失败结果。
调用关系:它和 tests::FakeBackend::apply_task 配套,都是为了满足 CloudBackend 接口。当前测试流程不会调用它。
调用图:外部调用 2 个(pin, Unimplemented)。
tests::FakeBackend::create_task496–509 ↗
fn create_task(
&'a self,
_env_id: &'a str,
_prompt: &'a str,
_git_ref: &'a str,
_qa_mode: bool,
_best_of_n: usize,
) ->
作用:测试假后端里的“新建任务”占位函数。当前测试不创建任务,所以它返回未实现错误。
数据流:进去的是环境 id、提示词、git 引用、是否 QA 模式、best-of 数量 → 它不保存也不发送这些信息,直接返回 Unimplemented 错误 → 出来是失败结果。
调用关系:它是 CloudBackend 接口的一部分,用来让 FakeBackend 能传给依赖完整后端的代码。环境筛选测试不会调用它。
调用图:外部调用 2 个(pin, Unimplemented)。
tests::load_tasks_uses_env_parameter513–533 ↗
async fn load_tasks_uses_env_parameter()
作用:验证 load_tasks 会把环境参数正确传给后端。简单说,它检查“选了哪个环境,就真的加载哪个环境的任务”。
数据流:进去没有外部输入 → 它先准备一份按环境分组的假数据,再用 FakeBackend 分别请求无环境、env-A、env-B → 出来是测试断言结果:每次返回的任务数量和标题都必须符合对应环境。
调用关系:它直接调用 load_tasks,并通过 FakeBackend 提供可控数据。这个测试保护了环境筛选功能,防止以后改代码时不小心把 env 参数漏传。
调用图:调用 1 个内部函数(load_tasks);外部调用 3 个(assert_eq!, new, vec!)。
cloud-tasks/src/new_task.rs源码 ↗
这个文件可以理解成“新建任务表单”的说明书和出厂设置。NewTaskPage 里放着用户正在输入的任务内容、当前是否正在提交、选中的环境编号 env_id,以及 best_of_n,也就是要让系统尝试几次再选最好结果。创建页面时,它会先做一个 ComposerInput,也就是给用户打字用的输入框,然后给输入框加上几条快捷键提示,比如回车发送、Shift+回车换行、Ctrl+O 选择环境、Ctrl+N 调整尝试次数。这样用户一打开页面,就知道该怎么操作。Default 则提供一个最普通的默认页面:不预选环境,尝试次数为 1。这个文件本身不真正提交任务,也不和服务器通信,它主要负责把“新任务页面刚出现时应该长什么样、有哪些初始值”固定下来。
NewTaskPage::new11–26 ↗
fn new(env_id: Option<String>, best_of_n: usize) -> Self
作用:创建一个新的“新建任务页面”。调用者可以传入预先选好的环境编号,以及希望生成多少个候选结果再挑最好的次数。
数据流:进去的是 env_id 和 best_of_n:env_id 可能有值,也可能没有;best_of_n 是尝试次数。函数先新建一个输入框 ComposerInput,然后给它塞入几条快捷键提示。最后产出一个 NewTaskPage:里面有配置好的输入框,submitting 被设为 false,表示还没开始提交,同时保存传进来的环境和尝试次数。
调用关系:这是这个页面的主要入口。别的地方需要显示新建任务页面时,会调用它来拿到一份准备好的页面状态。它自己会借用 ComposerInput::new 来做输入框,并用一个列表把快捷键提示一次性放进去。
cloud-tasks/src/ui.rs源码 ↗
这个文件就像终端界面的“排版师”。它不去真正创建任务、下载详情或应用代码改动,而是读取 App 里的当前状态,然后决定屏幕上该显示什么:普通任务列表、新建任务输入页、底部快捷键提示、加载中的小圆点、任务详情弹窗、环境选择弹窗、并行尝试次数弹窗,以及“是否应用改动”的确认框。它用 ratatui(一个在终端里画窗口、列表、文字颜色的库)来切区域、画边框、上色。重要的是,它会根据用户当前焦点来遮罩背景、显示滚动百分比、把 diff 里新增行染绿、删除行染红,还会把对话内容按 User 和 Assistant 分组,避免用户在一堆纯文本里迷路。
draw28–57 ↗
fn draw(frame: &mut Frame, app: &mut App)
作用:这是整个界面的总入口。每次终端需要刷新画面时,它看一眼 App 当前状态,然后决定画任务列表还是新建任务页,再叠加各种弹窗。
数据流:输入是可绘制的 frame 和可读取/修改的 app 状态 → 它先把屏幕切成主区域和底部提示区,再按 app.new_task、diff_overlay、env_modal、best_of_modal、apply_modal 这些状态选择对应画法 → 输出是更新后的终端画面,同时可能更新光标位置或加载动画计时。
调用关系:它站在最外层,运行时由 TUI 主循环反复调用。它把具体绘制工作分给 draw_new_task_page、draw_list、draw_footer、draw_diff_overlay、draw_env_modal、draw_best_of_modal 和 draw_apply_modal。
调用图:调用 8 个内部函数(draw_apply_modal, draw_best_of_modal, draw_diff_overlay, draw_env_modal, draw_footer, draw_list, draw_new_task_page, area);外部调用 3 个(Length, Min, default)。
rounded_enabled62–69 ↗
fn rounded_enabled() -> bool
作用:这个函数决定弹窗边框要不要用圆角。它让用户可以通过环境变量开关视觉样式。
数据流:输入来自环境变量 CODEX_TUI_ROUNDED → 第一次调用时读取并判断是否等于 1,没设置时默认启用圆角 → 输出一个布尔值,并用 OnceLock 缓存起来,之后不用重复读环境变量。
调用关系:它只被 overlay_block 使用。每次要创建弹窗边框时,overlay_block 会问它该用圆角还是普通方角。
调用图:被 1 处调用(overlay_block)。
overlay_outer71–88 ↗
fn overlay_outer(area: Rect) -> Rect
作用:这个函数算出弹窗应该放在屏幕中间哪块区域。它相当于给弹窗留出四周边距,避免弹窗贴满全屏。
数据流:输入是整个屏幕或父区域的矩形大小 → 它上下各留 10%、左右各留 10%,取中间 80% 的区域 → 输出一个新的矩形,供弹窗使用。
调用关系:各种弹窗都会先调用它,包括详情弹窗、环境选择、应用确认、并行次数选择。它不画东西,只负责告诉别人“弹窗框放这里”。
调用图:被 4 处调用(draw_apply_modal, draw_best_of_modal, draw_diff_overlay, draw_env_modal);外部调用 2 个(Percentage, default)。
overlay_block90–98 ↗
fn overlay_block() -> Block<'static>
作用:这个函数做出统一风格的弹窗外框。这样所有弹窗看起来像同一套界面,而不是各画各的。
数据流:输入不需要业务数据 → 它创建带边框的 Block,按 rounded_enabled 的结果决定是否圆角,并加上内部留白 → 输出一个可复用的弹窗边框对象。
调用关系:draw_diff_overlay、draw_env_modal、draw_best_of_modal、draw_apply_modal 都靠它画外壳;overlay_content 也用它来计算边框里面真正能放内容的区域。
调用图:调用 1 个内部函数(rounded_enabled);被 5 处调用(draw_apply_modal, draw_best_of_modal, draw_diff_overlay, draw_env_modal, overlay_content);外部调用 2 个(default, new)。
overlay_content100–102 ↗
fn overlay_content(area: Rect) -> Rect
作用:这个函数计算弹窗边框里面的内容区。简单说,就是扣掉边框和内边距后,文字列表真正能放在哪里。
数据流:输入是弹窗整体矩形 → 它临时创建同样样式的 overlay_block,并取这个块的 inner 区域 → 输出内容区域矩形。
调用关系:所有带弹窗内容的绘制函数都会用它。它保证标题、边框、留白不会挤到正文。
调用图:调用 1 个内部函数(overlay_block);被 4 处调用(draw_apply_modal, draw_best_of_modal, draw_diff_overlay, draw_env_modal)。
draw_new_task_page104–174 ↗
fn draw_new_task_page(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画“新建任务”的输入页面。用户按新建后,看到的标题、环境信息、尝试次数和输入框都由它画出来。
数据流:输入是 frame、要画的区域和 app → 它从 app.new_task 读取当前选择的环境、并行尝试次数和输入框内容,计算输入框需要多高,把输入框固定在页面底部 → 输出是新建任务页面,并把终端光标放到输入框希望的位置。
调用关系:draw 在发现 app.new_task 存在时调用它。它自己不提交任务,只把 composer 这个输入组件渲染出来,让用户继续编辑。
调用图:调用 3 个内部函数(area, buffer_mut, set_cursor_position);被 1 处调用(draw);外部调用 8 个(default, Length, Min, default, from, format!, render_widget, vec!)。
draw_list176–234 ↗
fn draw_list(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画主任务列表。用户平时看到的每个任务状态、标题、更新时间和改动摘要,都从这里出来。
数据流:输入是 frame、列表区域和 app → 它把 app.tasks 里的每个任务交给 render_task_item 变成列表项,再根据 app.selected 标出当前选中项;如果有弹窗打开,就把背景列表变暗;如果正在刷新,就在列表中间画加载提示 → 输出是可滚动感的任务列表画面。
调用关系:draw 在没有新建任务页时调用它。它会使用 render_task_item 格式化单个任务,也会在加载时把显示工作交给 draw_centered_spinner。
调用图:调用 1 个内部函数(draw_centered_spinner);被 1 处调用(draw);外部调用 12 个(default, Length, Min, default, from, new, default, default, format!, render_stateful_widget (+2 more))。
draw_diff_overlay312–467 ↗
fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画任务详情弹窗。它既能显示代码差异 diff(也就是改了哪些行),也能显示用户和助手的对话内容。
数据流:输入是 frame、屏幕区域和 app → 它读取 app.diff_overlay,判断当前内容能不能应用、是不是失败详情、滚动到哪里、当前看的是 Prompt 还是 Diff;然后画标题、顶部状态栏、尝试次数提示和正文;正文如果是 diff 就按增删行上色,如果是对话就按说话人排版;如果详情还在加载且没内容,就画居中加载提示 → 输出是覆盖在主界面上的详情窗口,并更新滚动内容的宽高设置。
调用关系:draw 在 app.diff_overlay 存在时调用它。它使用 overlay_outer、overlay_block、overlay_content 搭弹窗框架,用 style_diff_line 或 style_conversation_lines 给正文上色,加载时调用 draw_centered_spinner。
调用图:调用 4 个内部函数(draw_centered_spinner, overlay_block, overlay_content, overlay_outer);被 1 处调用(draw);外部调用 11 个(Length, Min, default, from, new, from, new, format!, matches!, render_widget (+1 more))。
draw_apply_modal469–550 ↗
fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画“是否把改动应用到本地”的确认弹窗。它让用户在真正改文件前看到确认、预检查结果和冲突信息。
数据流:输入是 frame、屏幕区域和 app → 它读取 app.apply_modal,显示任务标题和按键提示;如果正在预检查或应用,就显示加载;如果已有结果,就按成功、部分成功或错误给消息上色,并列出冲突文件和跳过文件 → 输出是应用确认/结果弹窗。
调用关系:draw 在 app.apply_modal 存在时调用它。它复用 overlay_outer、overlay_block、overlay_content 来画统一弹窗,忙碌时调用 draw_centered_spinner。
调用图:调用 4 个内部函数(draw_centered_spinner, overlay_block, overlay_content, overlay_outer);被 1 处调用(draw);外部调用 10 个(Length, Min, default, from, new, new, format!, matches!, render_widget, vec!)。
style_conversation_lines558–652 ↗
fn style_conversation_lines(
sd: &crate::scrollable_diff::ScrollableDiff,
attempt: Option<&AttemptView>,
) -> Vec<Line<'static>>
作用:这个函数把一段普通对话文本变成带颜色、带分组的终端显示行。它让 User 和 Assistant 的内容更容易区分。
数据流:输入是 ScrollableDiff 里的已换行文本和当前 attempt 信息 → 它逐行读取原始内容,识别 user:、assistant:、空行、代码块、项目符号和标题;再给每行加左侧竖线、说话人标题、状态标记和 markdown 样式 → 输出一组已经带样式的 Line。
调用关系:它在详情弹窗显示对话视图时被 draw_diff_overlay 间接使用。内部会调用 conversation_header_line、conversation_gutter_span 和 conversation_text_spans 来分别处理标题、左侧栏和正文。
调用图:调用 6 个内部函数(raw_line_at, wrapped_lines, wrapped_src_indices, conversation_gutter_span, conversation_header_line, conversation_text_spans);外部调用 4 个(from, raw, new, new)。
conversation_header_line654–678 ↗
fn conversation_header_line(
speaker: ConversationSpeaker,
attempt: Option<&AttemptView>,
) -> Line<'static>
作用:这个函数生成对话里一段话的标题行,比如“User prompt”或“Assistant response”。它还会给助手回复附上当前尝试的状态。
数据流:输入是说话人类型和可选的 attempt → 它按 User 或 Assistant 选择不同颜色和文字;如果是 Assistant 且有状态,就通过 attempt_status_span 转成状态标签 → 输出一行带样式的标题。
调用关系:style_conversation_lines 遇到 user: 或 assistant: 标记时调用它。它把状态显示这件事再交给 attempt_status_span。
调用图:调用 1 个内部函数(attempt_status_span);被 1 处调用(style_conversation_lines);外部调用 2 个(from, vec!)。
conversation_gutter_span680–685 ↗
fn conversation_gutter_span(speaker: ConversationSpeaker) -> ratatui::text::Span<'static>
作用:这个函数生成对话正文左边的小竖线。它像聊天气泡边框一样,帮助用户看出这行属于谁。
数据流:输入是说话人类型 → 它给 User 用青色竖线,给 Assistant 用紫色竖线 → 输出一个带颜色的 Span。
调用关系:style_conversation_lines 在处理每一行对话正文或空行时调用它。它只负责左侧视觉标记,不处理正文内容。
调用图:被 1 处调用(style_conversation_lines)。
conversation_text_spans687–740 ↗
fn conversation_text_spans(
display: &str,
in_code: bool,
is_new_raw: bool,
bullet_indent: Option<usize>,
) -> Vec<ratatui::text::Span<'static>>
作用:这个函数给对话正文上样式。代码块、项目符号、标题和普通 markdown 文本会用不同方式显示。
数据流:输入是一行要显示的文字,以及它是否在代码块里、是否是原始新行、是否属于项目符号缩进 → 它先处理代码块,再处理列表项和续行,再处理 markdown 标题,最后用 render_markdown_text 给普通文本做基础 markdown 渲染 → 输出这行文字对应的一组带样式片段。
调用关系:style_conversation_lines 会把每一行正文交给它。它依赖 codex_tui::render_markdown_text 来处理普通 markdown 的加粗、链接等基础显示效果。
调用图:被 1 处调用(style_conversation_lines);外部调用 5 个(raw, new, new, render_markdown_text, vec!)。
attempt_status_span742–751 ↗
fn attempt_status_span(status: AttemptStatus) -> Option<ratatui::text::Span<'static>>
作用:这个函数把一次尝试的状态变成有颜色的短标签。用户可以一眼看出尝试是完成、失败、进行中还是等待中。
数据流:输入是 AttemptStatus 枚举值 → 它按不同状态选择文字和颜色;未知状态不显示 → 输出可选的带样式 Span。
调用关系:conversation_header_line 在生成 Assistant 标题时调用它。它只做状态到显示标签的转换。
调用图:被 1 处调用(conversation_header_line)。
style_diff_line753–786 ↗
render_task_item788–844 ↗
fn render_task_item(_app: &App, t: &codex_cloud_tasks_client::TaskSummary) -> ListItem<'static>
作用:这个函数把一个任务摘要变成列表里的一项。它负责把机器数据整理成用户能扫一眼看懂的几行文字。
数据流:输入是 app 和一个 TaskSummary → 它读取任务状态、标题、环境标签、更新时间和改动摘要;把状态变成彩色标签,把更新时间转成“几分钟前”这种相对时间,把增删行数和文件数排成摘要 → 输出一个 ListItem,供任务列表显示。
调用关系:draw_list 会对 app.tasks 里的每个任务调用它。它使用 format_relative_time_now 把时间戳变成人话。
调用图:调用 1 个内部函数(format_relative_time_now);外部调用 4 个(from, new, new, vec!)。
draw_inline_spinner846–863 ↗
fn draw_inline_spinner(
frame: &mut Frame,
area: Rect,
spinner_start: &mut Option<Instant>,
label: &str,
)
作用:这个函数画一行小型加载提示,比如“• Loading…”。它用于告诉用户程序还在工作,不是卡死了。
数据流:输入是 frame、要画的位置、加载动画开始时间和标签文字 → 它如果还没有开始时间就记录当前时间,再按经过时间让圆点明暗交替 → 输出是一行加载提示,并可能初始化 spinner_start。
调用关系:draw_footer 直接用它画右下角加载状态;draw_centered_spinner 也借它画居中的加载提示。
调用图:被 2 处调用(draw_centered_spinner, draw_footer);外部调用 4 个(from, new, render_widget, vec!)。
draw_centered_spinner865–889 ↗
fn draw_centered_spinner(
frame: &mut Frame,
area: Rect,
spinner_start: &mut Option<Instant>,
label: &str,
)
作用:这个函数把加载提示放到某个区域正中间。适合列表、弹窗正文这类空白区域里的等待状态。
数据流:输入是 frame、区域、动画开始时间和标签文字 → 它把区域纵向和横向切开,取中间一小块 → 再调用 draw_inline_spinner 在那里画加载文字。
调用关系:draw_list、draw_diff_overlay、draw_apply_modal、draw_env_modal 在等待网络或后台操作时调用它。它自己不决定何时加载,只负责居中显示。
调用图:调用 1 个内部函数(draw_inline_spinner);被 4 处调用(draw_apply_modal, draw_diff_overlay, draw_env_modal, draw_list);外部调用 3 个(Length, Percentage, default)。
draw_env_modal893–991 ↗
fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画环境选择弹窗。用户可以搜索环境,选择只看某个环境的任务,或切回全部环境。
数据流:输入是 frame、屏幕区域和 app → 它先画统一弹窗;如果环境还在加载,就显示加载;否则显示操作提示、搜索框,并用搜索词过滤 app.environments;最后把“All Environments”和匹配到的环境列成可选择列表 → 输出是环境选择窗口和当前高亮项。
调用关系:draw 在 app.env_modal 存在时调用它。它复用 overlay_outer、overlay_block、overlay_content,加载时调用 draw_centered_spinner。
调用图:调用 4 个内部函数(draw_centered_spinner, overlay_block, overlay_content, overlay_outer);被 1 处调用(draw);外部调用 15 个(default, Length, Min, default, from, new, new, default, new, default (+5 more))。
draw_best_of_modal993–1046 ↗
fn draw_best_of_modal(frame: &mut Frame, area: Rect, app: &mut App)
作用:这个函数画“并行尝试次数”的选择弹窗。它让用户选择新任务要同时跑 1 到 4 个尝试。
数据流:输入是 frame、屏幕区域和 app → 它在屏幕中央算出一个较小弹窗,显示使用提示和 1、2、3、4 个 attempt 的选项;当前全局 best_of_n 会标成 Current,选中的行会高亮 → 输出是并行尝试次数选择窗口。
调用关系:draw 在 app.best_of_modal 存在时调用它。它也使用 overlay_outer、overlay_block、overlay_content 来保持弹窗风格一致。
调用图:调用 3 个内部函数(overlay_block, overlay_content, overlay_outer);被 1 处调用(draw);外部调用 16 个(default, Length, Min, default, from, new, new, default, new, new (+6 more))。
转录和导航覆盖层
这些文件提供可复用的覆盖层浏览,以及支持转录回溯和多智能体导航流程的状态与格式化逻辑。
tui/src/app_backtrack.rs源码 ↗
可以把它想成聊天软件里的“撤回到某一轮再改写”。用户按 Esc 会先进入准备状态,再打开完整聊天记录浮层,选中某条以前的用户消息;按 Enter 后,它不会立刻删本地记录,而是先向核心后台请求回滚。只有后台确认成功后,它才把本地聊天记录裁剪到对应位置。这样做很重要,因为如果后台回滚失败、或者用户已经切到另一个线程,本地界面就不能乱删记录。文件里还处理了聊天记录浮层的显示边界:浮层不只显示已经写入历史的内容,也会把当前正在生成的消息临时接到尾部显示,免得用户打开记录时觉得工具调用或流式输出“丢了”。
App::handle_backtrack_overlay_event113–171 ↗
async fn handle_backtrack_overlay_event(
&mut self,
tui: &mut tui::Tui,
event: TuiEvent,
) -> Result<bool>
作用:当聊天记录浮层打开时,这个函数决定键盘事件该怎么处理。它区分普通浏览记录和“回退预览”两种状态。
数据流:输入是一个界面事件,比如 Esc、左右方向键、Enter 或绘制事件。它先看当前是否正在回退预览:如果是,就把按键变成“往旧消息选”“往新消息选”“确认回退”等动作;如果不是,第一次 Esc 会开启回退预览,其他事件继续交给浮层处理。输出是一个布尔值,表示这个事件已经被处理掉了。
调用关系:它是浮层事件的门卫。需要进入预览时交给 App::begin_overlay_backtrack_preview,确认时交给 App::overlay_confirm_backtrack,移动选择时交给 App::overlay_step_backtrack 或 App::overlay_step_backtrack_forward,普通事件则交给 App::overlay_forward_event。
调用图:调用 5 个内部函数(begin_overlay_backtrack_preview, overlay_confirm_backtrack, overlay_forward_event, overlay_step_backtrack, overlay_step_backtrack_forward)。
App::handle_backtrack_esc_key174–186 ↗
fn handle_backtrack_esc_key(&mut self, tui: &mut tui::Tui)
作用:当主聊天界面里按 Esc 时,这个函数启动或推进“回到上一条问题”的流程。它只在输入框为空时生效,避免误把正在写的内容覆盖掉。
数据流:输入是当前界面和 TUI 控制对象。它检查输入框是否为空;第一次 Esc 会记录当前线程并显示提示,第二次 Esc 会打开聊天记录浮层并选中可回退的消息;如果浮层已经处于预览状态,就继续往更早的用户消息移动。它主要改变 App 里的回退状态和界面显示。
调用关系:它是主界面 Esc 的入口。第一次调用 App::prime_backtrack,之后调用 App::open_backtrack_preview,预览中继续调用 App::step_backtrack_and_highlight。
调用图:调用 3 个内部函数(open_backtrack_preview, prime_backtrack, step_backtrack_and_highlight)。
App::apply_backtrack_rollback195–240 ↗
fn apply_backtrack_rollback(&mut self, selection: BacktrackSelection)
作用:这个函数真正发起一次“请后台把线程回滚到某条用户消息之前”的请求。它还会把选中的旧问题先填回输入框,方便用户马上修改。
数据流:输入是一份 BacktrackSelection,里面有选中的第几条用户消息、文本、图片等。它先拒绝侧边会话和重复回滚,再根据当前用户消息总数算出要回滚多少轮,向后台提交 thread_rollback 命令,并把这次请求记为 pending_rollback。输出不是直接返回结果,而是改变界面状态:输入框被预填,等待后台确认。
调用关系:它是回退确认后的核心动作。App::overlay_confirm_backtrack、App::apply_backtrack_selection 和 App::apply_cancelled_turn_edit 会调用它;它会用 user_count 计算轮数,必要时用 App::reset_backtrack_state 清理状态,并把 rollback 命令交给聊天组件发送。
调用图:调用 2 个内部函数(reset_backtrack_state, user_count);被 3 处调用(apply_backtrack_selection, apply_cancelled_turn_edit, overlay_confirm_backtrack);外部调用 2 个(thread_rollback, try_from)。
App::apply_cancelled_turn_edit242–272 ↗
fn apply_cancelled_turn_edit(&mut self, prompt: UserMessage)
作用:当用户取消某一轮编辑时,这个函数把那条原本要编辑的消息放回输入框,并尝试让后台回滚相应的一轮。
数据流:输入是一条 UserMessage,包含文本和图片。它先看本地历史里有多少用户消息;如果没有用户历史,也会发起一次回滚请求并恢复输入框;如果有,就构造一个回退选择并走正常回退流程。结果是输入框恢复成那条消息,后台可能收到回滚命令。
调用关系:它复用 App::apply_backtrack_rollback 来做主要回滚工作;在特殊的空历史场景下直接提交 thread_rollback 命令。
调用图:调用 2 个内部函数(apply_backtrack_rollback, user_count);外部调用 1 个(thread_rollback)。
App::open_transcript_overlay275–282 ↗
fn open_transcript_overlay(&mut self, tui: &mut tui::Tui)
作用:打开完整聊天记录浮层,让用户可以在一个单独的全屏视图里看历史。回退预览也依赖这个浮层来高亮旧消息。
数据流:输入是 TUI 控制对象。它让终端进入备用屏幕,创建一个包含当前 transcript_cells 的 Transcript 浮层,并请求重新绘制。结果是主界面被浮层覆盖,用户看到完整记录。
调用关系:它通常由 App::open_backtrack_preview 调用。创建浮层时会调用 new_transcript,并通过 TUI 的 frame_requester 安排下一帧显示。
调用图:调用 1 个内部函数(new_transcript);被 1 处调用(open_backtrack_preview);外部调用 2 个(enter_alt_screen, frame_requester)。
App::close_transcript_overlay285–302 ↗
fn close_transcript_overlay(&mut self, tui: &mut tui::Tui)
作用:关闭聊天记录浮层,回到正常聊天界面。它还会处理浮层期间暂存的历史行,避免显示状态乱掉。
数据流:输入是 TUI 控制对象。它离开备用屏幕,把浮层打开期间延迟写入的历史行刷回主界面,清掉 overlay 和预览标记,并请求重绘。如果关闭的是回退预览,还会彻底重置回退状态。
调用关系:App::begin_overlay_backtrack_preview 在没有可回退目标时会调用它;App::overlay_confirm_backtrack 确认前会调用它;App::overlay_forward_event 发现浮层结束时也会调用它。
调用图:调用 1 个内部函数(reset_backtrack_state);被 3 处调用(begin_overlay_backtrack_preview, overlay_confirm_backtrack, overlay_forward_event);外部调用 4 个(frame_requester, insert_history_hyperlink_lines_with_wrap_policy, leave_alt_screen, take)。
App::prime_backtrack305–312 ↗
fn prime_backtrack(&mut self)
作用:这是回退流程的“预备动作”。第一次按 Esc 时,它记住当前线程,并在有可回退消息时给用户提示。
数据流:它读取当前线程 id 和聊天记录。然后把 backtrack.primed 设为 true,把选中位置设为“尚未选择”,并保存 base_id。若历史里有用户消息,就让输入区显示 Esc 回退提示。
调用关系:它由 App::handle_backtrack_esc_key 在第一次 Esc 时调用;它通过 has_backtrack_target 判断是否值得提示。
调用图:调用 1 个内部函数(has_backtrack_target);被 1 处调用(handle_backtrack_esc_key)。
App::open_backtrack_preview315–329 ↗
fn open_backtrack_preview(&mut self, tui: &mut tui::Tui)
作用:这个函数打开回退预览:显示完整聊天记录,并选中最近的一条用户消息。没有可回退消息时,它会告诉用户“没有上一条可编辑”。
数据流:它读取 transcript_cells。若没有用户消息,就清理回退状态、显示提示并请求重绘;若有,就打开记录浮层,进入 overlay_preview_active 状态,清掉输入框提示,然后高亮一条可回退消息。
调用关系:它由 App::handle_backtrack_esc_key 在回退已经预备好后调用。它内部会调用 App::open_transcript_overlay、App::step_backtrack_and_highlight、App::reset_backtrack_state 和 has_backtrack_target。
调用图:调用 4 个内部函数(open_transcript_overlay, reset_backtrack_state, step_backtrack_and_highlight, has_backtrack_target);被 1 处调用(handle_backtrack_esc_key);外部调用 1 个(frame_requester)。
App::begin_overlay_backtrack_preview332–349 ↗
fn begin_overlay_backtrack_preview(&mut self, tui: &mut tui::Tui)
作用:当用户已经在聊天记录浮层里时,按 Esc 会通过这个函数进入回退预览,并默认选中最新的用户消息。
数据流:它检查当前历史中是否有用户消息。没有的话关闭浮层并显示“没有上一条可编辑”;有的话记录当前线程,打开预览状态,计算用户消息数量,选中最后一条,并请求重绘。
调用关系:它由 App::handle_backtrack_overlay_event 在浮层里第一次按 Esc 时调用;它会用 user_count 找最后一条用户消息,并用 App::apply_backtrack_selection_internal 更新高亮。
调用图:调用 4 个内部函数(apply_backtrack_selection_internal, close_transcript_overlay, has_backtrack_target, user_count);被 1 处调用(handle_backtrack_overlay_event);外部调用 1 个(frame_requester)。
App::step_backtrack_and_highlight352–372 ↗
fn step_backtrack_and_highlight(&mut self, tui: &mut tui::Tui)
作用:把回退选择往更早的一条用户消息移动,并在浮层里高亮出来。就像在历史记录里往上翻一个可编辑节点。
数据流:它读取当前用户消息数量和当前选中的第几条。若还没选过,就选最新一条;若已经在最早一条,就停住;否则往前移一条。然后更新内部选择和浮层高亮,并请求重绘。
调用关系:主界面继续按 Esc、刚打开预览、或浮层里按 Esc/左键时都会用它。它把最终选择交给 App::apply_backtrack_selection_internal。
调用图:调用 2 个内部函数(apply_backtrack_selection_internal, user_count);被 3 处调用(handle_backtrack_esc_key, open_backtrack_preview, overlay_step_backtrack);外部调用 1 个(frame_requester)。
App::step_forward_backtrack_and_highlight375–393 ↗
fn step_forward_backtrack_and_highlight(&mut self, tui: &mut tui::Tui)
作用:把回退选择往更新的一条用户消息移动。它用于用户在预览里按右键,想从太旧的位置退回来一点。
数据流:它读取用户消息数量和当前选中位置。没有用户消息就不做事;否则从当前位置往后加一,但不会超过最新一条。然后更新高亮并请求重绘。
调用关系:它由 App::overlay_step_backtrack_forward 调用,实际更新动作交给 App::apply_backtrack_selection_internal。
调用图:调用 2 个内部函数(apply_backtrack_selection_internal, user_count);被 1 处调用(overlay_step_backtrack_forward);外部调用 1 个(frame_requester)。
App::apply_backtrack_selection_internal396–408 ↗
fn apply_backtrack_selection_internal(&mut self, nth_user_message: usize)
作用:把“第几条用户消息”转换成聊天记录里的具体格子位置,并让浮层高亮它。它是选择状态和画面高亮之间的桥。
数据流:输入是 nth_user_message,也就是从当前会话开始算的第几条用户消息。它通过 nth_user_position 找到 transcript_cells 里的实际下标;找到就保存这个选择并设置高亮,找不到就清空选择和高亮。
调用关系:它被 App::begin_overlay_backtrack_preview、App::step_backtrack_and_highlight、App::step_forward_backtrack_and_highlight 和 App::sync_overlay_after_transcript_trim 调用,用来保持选择与浮层画面一致。
调用图:调用 1 个内部函数(nth_user_position);被 4 处调用(begin_overlay_backtrack_preview, step_backtrack_and_highlight, step_forward_backtrack_and_highlight, sync_overlay_after_transcript_trim)。
App::overlay_forward_event423–459 ↗
fn overlay_forward_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()>
作用:把不属于回退快捷键的事件交给浮层自己处理。遇到绘制事件时,它还会把当前正在生成的聊天内容临时显示在记录浮层尾部。
数据流:输入是一个 TUI 事件。若是绘制或窗口大小变化,并且当前是聊天记录浮层,它会从 ChatWidget 取正在活动的消息缓存键和显示行,交给浮层同步 live tail,然后绘制;如果有动画且滚动到底部,会安排下一帧。其他事件则直接交给 overlay.handle_event。若浮层表示自己结束了,就关闭浮层。
调用关系:它被 App::handle_backtrack_overlay_event、App::overlay_step_backtrack 和 App::overlay_step_backtrack_forward 当作默认转发通道使用;必要时调用 App::close_transcript_overlay 收尾。
调用图:调用 1 个内部函数(close_transcript_overlay);被 3 处调用(handle_backtrack_overlay_event, overlay_step_backtrack, overlay_step_backtrack_forward);外部调用 4 个(draw, frame_requester, matches!, from_millis)。
App::overlay_confirm_backtrack462–470 ↗
fn overlay_confirm_backtrack(&mut self, tui: &mut tui::Tui)
作用:在回退预览浮层里按 Enter 时,这个函数确认当前选择。它关闭浮层,然后发起真正的回滚请求。
数据流:它读取当前选中的 nth_user_message,尝试生成 BacktrackSelection,然后关闭聊天记录浮层。如果选择有效,就调用回滚逻辑,并请求界面重绘。
调用关系:它由 App::handle_backtrack_overlay_event 在 Enter 键时调用;它依赖 App::backtrack_selection 取出旧消息内容,再交给 App::apply_backtrack_rollback。
调用图:调用 3 个内部函数(apply_backtrack_rollback, backtrack_selection, close_transcript_overlay);被 1 处调用(handle_backtrack_overlay_event);外部调用 1 个(frame_requester)。
App::overlay_step_backtrack473–480 ↗
fn overlay_step_backtrack(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()>
作用:处理回退预览浮层里的 Esc 或左方向键:如果回退流程还有效,就往更早的消息移动;否则当普通浮层事件处理。
数据流:输入是 TUI 控制对象和原始事件。它检查 base_id 是否存在;存在说明回退已准备好,于是移动选择并高亮;不存在就把事件原样转发给浮层。结果是选择变化,或者浮层自己处理该事件。
调用关系:它由 App::handle_backtrack_overlay_event 调用。正常走 App::step_backtrack_and_highlight,异常或未准备时走 App::overlay_forward_event。
调用图:调用 2 个内部函数(overlay_forward_event, step_backtrack_and_highlight);被 1 处调用(handle_backtrack_overlay_event)。
App::overlay_step_backtrack_forward483–494 ↗
fn overlay_step_backtrack_forward(
&mut self,
tui: &mut tui::Tui,
event: TuiEvent,
) -> Result<()>
作用:处理回退预览浮层里的右方向键:如果流程有效,就往更新的消息移动;否则交给浮层普通处理。
数据流:输入是 TUI 控制对象和事件。它根据 base_id 判断是否处在有效回退流程中;有效就调用向前移动逻辑,无效就转发事件。结果是高亮位置向更新的用户消息移动,或浮层自行响应。
调用关系:它由 App::handle_backtrack_overlay_event 调用;主要把工作交给 App::step_forward_backtrack_and_highlight,兜底交给 App::overlay_forward_event。
调用图:调用 2 个内部函数(overlay_forward_event, step_forward_backtrack_and_highlight);被 1 处调用(handle_backtrack_overlay_event)。
App::confirm_backtrack_from_main498–502 ↗
fn confirm_backtrack_from_main(&mut self) -> Option<BacktrackSelection>
作用:在没有浮层的主界面里确认一个已经准备好的回退选择。它返回要回退的旧消息内容,供外层继续处理。
数据流:它读取当前 backtrack.nth_user_message,尝试生成 BacktrackSelection,然后重置回退状态。输出是一个可选的选择结果:线程不匹配或找不到内容时为 None。
调用关系:它调用 App::backtrack_selection 组装选择,再调用 App::reset_backtrack_state 收尾。
调用图:调用 2 个内部函数(backtrack_selection, reset_backtrack_state)。
App::reset_backtrack_state505–511 ↗
fn reset_backtrack_state(&mut self)
作用:清空所有回退相关的临时状态。它相当于把“正在准备回退”的开关全部关掉。
数据流:它不需要额外输入,只修改 App 内部状态:primed 变 false,base_id 清空,nth_user_message 设为无选择,并清掉输入框里的 Esc 回退提示。
调用关系:很多流程结束或失败时都会调用它,比如 App::apply_backtrack_rollback、App::close_transcript_overlay、App::confirm_backtrack_from_main 和 App::open_backtrack_preview。
调用图:被 4 处调用(apply_backtrack_rollback, close_transcript_overlay, confirm_backtrack_from_main, open_backtrack_preview)。
App::apply_backtrack_selection513–520 ↗
fn apply_backtrack_selection(
&mut self,
tui: &mut tui::Tui,
selection: BacktrackSelection,
)
作用:这是一个小包装函数:拿到用户确认的回退选择后,发起回滚并安排界面刷新。
数据流:输入是 TUI 控制对象和 BacktrackSelection。它把选择交给 App::apply_backtrack_rollback,然后请求下一帧绘制。结果是后台收到回滚请求,界面准备更新。
调用关系:它本身不做复杂判断,主要把外部确认的选择接到 App::apply_backtrack_rollback 这条主流程上。
调用图:调用 1 个内部函数(apply_backtrack_rollback);外部调用 1 个(frame_requester)。
App::handle_backtrack_rollback_succeeded522–529 ↗
fn handle_backtrack_rollback_succeeded(&mut self, num_turns: u32)
作用:后台确认回滚成功时,这个函数决定本地该怎么跟上。它区分“这是我刚刚发起的回退”还是“别处传来的线程回滚”。
数据流:输入是后台说回滚了多少轮。若 App 里有 pending_rollback,说明这是本界面发起的回退,就完成本地裁剪;否则发送一个 AppEvent,让事件队列按顺序处理非本地待确认的回滚。结果是本地历史稍后被正确裁剪。
调用关系:它在收到 rollback succeeded 事件时被调用。有待处理请求时交给 App::finish_pending_backtrack;没有时把 ApplyThreadRollback 事件发回应用事件流。
调用图:调用 1 个内部函数(finish_pending_backtrack)。
App::handle_backtrack_rollback_failed531–533 ↗
fn handle_backtrack_rollback_failed(&mut self)
作用:后台说回滚失败时,这个函数取消“等待回滚确认”的状态,避免界面一直卡着不让用户再次尝试。
数据流:它不需要输入,只把 backtrack.pending_rollback 清空。它不会撤掉已经填进输入框的旧消息,因为那仍然可能方便用户修改后重试。
调用关系:它是失败事件的收尾点,和 App::handle_backtrack_rollback_succeeded 形成一成功一失败的配对。
App::apply_non_pending_thread_rollback539–550 ↗
fn apply_non_pending_thread_rollback(&mut self, num_turns: u32) -> bool
作用:处理那些不是当前界面主动发起、但后台通知需要回滚的情况。它按“删掉最后 N 个用户回合”的规则裁剪本地聊天记录。
数据流:输入是要回滚的用户轮数 num_turns。它尝试从 transcript_cells 末尾按用户消息边界裁剪;如果没有变化就返回 false。裁剪成功后,它清掉一些待刷新的提示,缩短复制用的助手历史,同步浮层,并标记需要重新渲染。输出是是否真的改动了本地记录。
调用关系:它会调用 trim_transcript_cells_drop_last_n_user_turns 做实际裁剪,调用 user_count 更新助手历史长度,再用 App::sync_overlay_after_transcript_trim 保持浮层一致。
调用图:调用 3 个内部函数(sync_overlay_after_transcript_trim, trim_transcript_cells_drop_last_n_user_turns, user_count)。
App::finish_pending_backtrack556–575 ↗
fn finish_pending_backtrack(&mut self)
作用:完成一次本界面发起且后台已经确认的回退。它只在确认事件属于当前线程时才裁剪本地记录。
数据流:它取出 pending_rollback。若这次请求的 thread_id 和当前聊天线程不一样,就什么也不删,防止切换会话后误删。若匹配,就把 transcript_cells 截到选中用户消息之前,清理刷新提示,调整助手历史,同步浮层,并标记需要重绘。
调用关系:它由 App::handle_backtrack_rollback_succeeded 调用。实际裁剪交给 trim_transcript_cells_to_nth_user,裁剪后的界面对齐交给 App::sync_overlay_after_transcript_trim。
调用图:调用 3 个内部函数(sync_overlay_after_transcript_trim, trim_transcript_cells_to_nth_user, user_count);被 1 处调用(handle_backtrack_rollback_succeeded)。
App::backtrack_selection577–604 ↗
fn backtrack_selection(&self, nth_user_message: usize) -> Option<BacktrackSelection>
作用:根据当前选中的“第几条用户消息”,取出那条旧消息的文本和图片,打包成可执行回退的选择。
数据流:输入是 nth_user_message。它先确认当初准备回退时的线程 id 仍然等于当前线程;不一致就返回 None。然后找到对应 UserHistoryCell,复制其中的文本、文本元素、本地图片路径和远程图片地址。输出是 BacktrackSelection。
调用关系:App::overlay_confirm_backtrack 和 App::confirm_backtrack_from_main 会调用它。它通过 nth_user_position 从抽象的用户消息编号找到真实历史格子。
调用图:调用 1 个内部函数(nth_user_position);被 2 处调用(confirm_backtrack_from_main, overlay_confirm_backtrack)。
App::sync_overlay_after_transcript_trim613–631 ↗
fn sync_overlay_after_transcript_trim(&mut self)
作用:当聊天记录被裁剪后,这个函数把所有相关界面状态同步一下,避免浮层还显示已经删掉的内容。
数据流:它读取新的 transcript_cells。若聊天记录浮层还开着,就替换浮层里的记录;若回退预览还在,就把高亮选择限制在仍然存在的用户消息范围内;最后清空浮层期间暂存的历史行,防止之后把已删除内容刷回主界面。
调用关系:它由 App::apply_non_pending_thread_rollback 和 App::finish_pending_backtrack 在裁剪成功后调用;需要重算高亮时会调用 App::apply_backtrack_selection_internal。
调用图:调用 2 个内部函数(apply_backtrack_selection_internal, user_count);被 2 处调用(apply_non_pending_thread_rollback, finish_pending_backtrack)。
trim_transcript_cells_to_nth_user634–648 ↗
fn trim_transcript_cells_to_nth_user(
transcript_cells: &mut Vec<Arc<dyn crate::history_cell::HistoryCell>>,
nth_user_message: usize,
) -> bool
作用:把聊天记录裁剪到某条用户消息之前。也就是说,选中的那条用户消息和它之后的内容都会被删掉。
数据流:输入是一组可修改的历史格子和 nth_user_message。它先拒绝“无选择”的特殊值,再找到第 nth 条用户消息在数组里的位置,把数组截断到那个位置。输出是布尔值,表示数组长度是否真的变了。
调用关系:App::finish_pending_backtrack 用它在后台确认后裁剪本地记录;多个测试函数也用它验证不同位置裁剪是否正确。它依赖 nth_user_position 找切割点。
调用图:调用 1 个内部函数(nth_user_position);被 4 处调用(finish_pending_backtrack, trim_transcript_for_first_user_drops_user_and_newer_cells, trim_transcript_for_later_user_keeps_prior_history, trim_transcript_preserves_cells_before_selected_user)。
trim_transcript_cells_drop_last_n_user_turns650–672 ↗
fn trim_transcript_cells_drop_last_n_user_turns(
transcript_cells: &mut Vec<Arc<dyn crate::history_cell::HistoryCell>>,
num_turns: u32,
) -> bool
作用:按“从末尾删掉 N 个用户回合”的规则裁剪聊天记录。它适合处理后台只告诉你回滚了几轮,而不是告诉你选中了哪条消息的情况。
数据流:输入是历史格子和要删的用户轮数。它找出当前会话里所有用户消息的位置;如果要删的轮数大于已有用户消息数,就从第一条用户消息前截断;否则从倒数第 N 条用户消息前截断。输出是是否真的删掉了内容。
调用关系:App::apply_non_pending_thread_rollback 调用它处理非 pending 的线程回滚;相关测试验证了普通回滚和超大回滚数的情况。它使用 user_positions_iter 找所有用户消息位置。
调用图:调用 1 个内部函数(user_positions_iter);被 3 处调用(apply_non_pending_thread_rollback, trim_drop_last_n_user_turns_allows_overflow, trim_drop_last_n_user_turns_applies_rollback_semantics);外部调用 1 个(try_from)。
user_count674–676 ↗
fn user_count(cells: &[Arc<dyn crate::history_cell::HistoryCell>]) -> usize
作用:数一数当前会话里有多少条用户消息。这里的“当前会话”指最后一个会话开始标记之后的内容。
数据流:输入是一组历史格子。它通过 user_positions_iter 找出所有用户消息位置,然后返回数量。它不修改任何数据。
调用关系:很多回退流程都用它判断有没有可回退目标、计算回滚轮数、或在裁剪后更新状态,比如 App::apply_backtrack_rollback、App::step_backtrack_and_highlight 和 App::finish_pending_backtrack。
调用图:调用 1 个内部函数(user_positions_iter);被 10 处调用(backtrack_selection_with_duplicate_history_targets_unique_turn, apply_backtrack_rollback, apply_cancelled_turn_edit, apply_non_pending_thread_rollback, begin_overlay_backtrack_preview, finish_pending_backtrack, step_backtrack_and_highlight, step_forward_backtrack_and_highlight, sync_overlay_after_transcript_trim, has_backtrack_target)。
has_backtrack_target678–680 ↗
fn has_backtrack_target(cells: &[Arc<dyn crate::history_cell::HistoryCell>]) -> bool
作用:判断当前聊天记录里是否有可以回退编辑的用户消息。没有用户消息就没法“编辑上一条”。
数据流:输入是历史格子列表。它调用 user_count,如果数量大于 0 就返回 true,否则返回 false。它不改动数据。
调用关系:App::prime_backtrack、App::open_backtrack_preview 和 App::begin_overlay_backtrack_preview 都用它决定是否显示提示、打开预览或提示用户没有可编辑消息。
调用图:调用 1 个内部函数(user_count);被 3 处调用(begin_overlay_backtrack_preview, open_backtrack_preview, prime_backtrack)。
nth_user_position682–689 ↗
fn nth_user_position(
cells: &[Arc<dyn crate::history_cell::HistoryCell>],
nth: usize,
) -> Option<usize>
作用:把“第几条用户消息”转换成历史数组里的真实下标。因为历史里还混着助手消息、系统信息等,所以不能直接用同一个数字当数组下标。
数据流:输入是历史格子和 nth。它遍历当前会话中的用户消息位置,找到序号等于 nth 的那个位置并返回;找不到就返回 None。
调用关系:App::apply_backtrack_selection_internal、App::backtrack_selection 和 trim_transcript_cells_to_nth_user 都靠它把用户选择定位到具体历史格子。
调用图:调用 1 个内部函数(user_positions_iter);被 3 处调用(apply_backtrack_selection_internal, backtrack_selection, trim_transcript_cells_to_nth_user)。
user_positions_iter691–708 ↗
fn user_positions_iter(
cells: &[Arc<dyn crate::history_cell::HistoryCell>],
) -> impl Iterator<Item = usize> + '_
作用:生成当前会话里所有用户消息在历史数组中的位置。它是许多计数和裁剪函数的底层工具。
数据流:输入是历史格子列表。它先找到最后一个 SessionInfoCell,也就是最近一次会话开始标记;然后只看它之后的格子,筛出 UserHistoryCell 的下标。输出是一个迭代器,可以逐个拿到这些位置。
调用关系:user_count、nth_user_position 和 trim_transcript_cells_drop_last_n_user_turns 都使用它。它保证回退只针对最近会话之后的用户消息,不误碰更早会话。
调用图:被 3 处调用(nth_user_position, trim_transcript_cells_drop_last_n_user_turns, user_count)。
agent_group_count711–713 ↗
fn agent_group_count(cells: &[Arc<dyn crate::history_cell::HistoryCell>]) -> usize
作用:这是测试用的小工具,用来数当前会话里有多少个助手消息组。它只在测试编译时存在。
数据流:输入是历史格子。它调用 agent_group_positions_iter 找出助手消息组的位置,然后返回数量。它不修改数据。
调用关系:它主要服务测试,特别是验证某些信息提示不会被误算成助手消息组。
调用图:调用 1 个内部函数(agent_group_positions_iter)。
agent_group_positions_iter716–736 ↗
fn agent_group_positions_iter(
cells: &[Arc<dyn crate::history_cell::HistoryCell>],
) -> impl Iterator<Item = usize> + '_
作用:这是测试用的底层工具,用来找当前会话里每个助手消息组的起始位置。它会忽略流式续写的片段。
数据流:输入是历史格子。它从最后一个会话开始标记之后遍历,只挑出 AgentMessageCell,并且要求它不是流式消息的延续部分。输出是这些助手消息组起点的下标迭代器。
调用关系:它只被测试辅助函数 agent_group_count 调用,用来让测试更精确地检查助手消息分组。
调用图:被 1 处调用(agent_group_count)。
tests::render_lines747–757 ↗
fn render_lines(lines: &[Line<'static>]) -> Vec<String>
作用:测试辅助函数,把界面行对象转成普通字符串,方便做快照或断言比较。
数据流:输入是一组 ratatui 的 Line。它把每行里的多个 span 文字拼起来,输出 Vec<String>。它不改变原始行。
调用关系:tests::backtrack_unavailable_info_message_snapshot 用它把提示消息渲染结果转成可比较文本。
调用图:外部调用 1 个(iter)。
tests::trim_transcript_for_first_user_drops_user_and_newer_cells760–776 ↗
fn trim_transcript_for_first_user_drops_user_and_newer_cells()
作用:这个测试确认:如果回退到第一条用户消息,那么这条用户消息和之后的助手回复都会被删掉。
数据流:它构造一段包含用户消息和助手消息的历史,然后调用 trim_transcript_cells_to_nth_user 选择第 0 条用户消息。之后断言历史变为空。
调用关系:它直接验证 trim_transcript_cells_to_nth_user 的最早边界行为,防止回退第一条时留下不该留的内容。
调用图:调用 1 个内部函数(trim_transcript_cells_to_nth_user);外部调用 2 个(assert!, vec!)。
tests::trim_transcript_preserves_cells_before_selected_user779–811 ↗
fn trim_transcript_preserves_cells_before_selected_user()
作用:这个测试确认:裁剪到某条用户消息前时,更早的非用户内容会保留下来。
数据流:它构造“助手开场、用户消息、助手后续”的历史,裁剪到第 0 条用户消息前。然后检查只剩下开场助手消息,并确认文字内容正确。
调用关系:它测试 trim_transcript_cells_to_nth_user 不会粗暴清空全部历史,而是按选中用户消息的位置精确截断。
调用图:调用 1 个内部函数(trim_transcript_cells_to_nth_user);外部调用 2 个(assert_eq!, vec!)。
tests::trim_transcript_for_later_user_keeps_prior_history814–873 ↗
fn trim_transcript_for_later_user_keeps_prior_history()
作用:这个测试确认:回退到较晚的一条用户消息时,之前完整的对话历史会保留。
数据流:它构造包含两条用户消息和多条助手消息的历史,裁剪到第二条用户消息前。然后断言第一条用户消息以及它之前、之间的助手消息都还在。
调用关系:它验证 trim_transcript_cells_to_nth_user 对中间位置的裁剪规则,保证只删除选中用户消息及其后面内容。
调用图:调用 1 个内部函数(trim_transcript_cells_to_nth_user);外部调用 2 个(assert_eq!, vec!)。
tests::trim_drop_last_n_user_turns_applies_rollback_semantics876–910 ↗
fn trim_drop_last_n_user_turns_applies_rollback_semantics()
作用:这个测试确认:按“删掉最后 N 个用户回合”的规则回滚时,会从最后一条用户消息前切掉后续内容。
数据流:它构造两轮用户和助手历史,调用 trim_transcript_cells_drop_last_n_user_turns 删除 1 轮。然后检查只剩第一轮相关内容。
调用关系:它验证 App::apply_non_pending_thread_rollback 依赖的底层裁剪函数是否符合后台 rollback 的语义。
调用图:调用 1 个内部函数(trim_transcript_cells_drop_last_n_user_turns);外部调用 3 个(assert!, assert_eq!, vec!)。
tests::trim_drop_last_n_user_turns_allows_overflow913–946 ↗
fn trim_drop_last_n_user_turns_allows_overflow()
作用:这个测试确认:如果要求删除的用户轮数远大于现有数量,函数也不会崩,而是尽可能安全地裁剪。
数据流:它构造一段含助手开场和一条用户消息的历史,传入 u32::MAX 作为删除轮数。函数应该从第一条用户消息前截断,保留更早的助手开场。测试再检查结果。
调用关系:它保护 trim_transcript_cells_drop_last_n_user_turns 的极端输入行为,防止大数字造成越界或错误裁剪。
调用图:调用 1 个内部函数(trim_transcript_cells_drop_last_n_user_turns);外部调用 3 个(assert!, assert_eq!, vec!)。
tests::agent_group_count_ignores_context_compacted_marker949–966 ↗
fn agent_group_count_ignores_context_compacted_marker()
作用:这个测试确认:类似“Context compacted”的信息提示不会被当成助手消息组来计数。
数据流:它构造两个助手消息,中间夹一个信息事件,然后调用 agent_group_count。期望结果是 2,而不是把信息事件也算进去。
调用关系:它验证测试辅助计数逻辑 agent_group_count 和 agent_group_positions_iter 对消息类型的识别是准确的。
调用图:外部调用 2 个(assert_eq!, vec!)。
tests::backtrack_target_requires_user_message969–991 ↗
tui/src/multi_agents.rs源码 ↗
可以把这个文件理解成多智能体界面的“字幕组”和“遥控器按键说明”。后端会发来一些比较机器化的事件,比如“启动了某个 agent”“给某个 agent 发了输入”“等待这些 agent 完成”。这里把这些事件翻译成人能读懂的历史记录行,比如“Spawned Robie [explorer]”“Finished waiting”,还会给名字、状态、错误信息加颜色。它也会截短太长的提示词和回复,避免终端被刷满。另一个重要部分是快捷键:用 Alt+左/右在智能体之间切换,并照顾 macOS 终端有时把 Option+方向键发成 Option+b/f 的情况。真正决定哪个线程成为当前线程、什么时候关闭线程,不在这里;这里只负责“怎么显示、怎么识别按键”。
agent_picker_status_dot_spans75–82 ↗
fn agent_picker_status_dot_spans(is_closed: bool) -> Vec<Span<'static>>
作用:给智能体选择列表里的状态小圆点准备显示内容。运行中的智能体显示绿色点,已关闭的只显示普通点,让用户一眼看出活跃程度。
数据流:输入是一个“是否已关闭”的布尔值 → 函数决定圆点要不要变绿,并在后面加一个空格 → 输出一组可被终端界面渲染的文字片段。
调用关系:它属于智能体选择器的显示小零件,通常会被列表行渲染代码拿来拼在名字前面;它不再把工作交给本文件里的其他函数。
调用图:外部调用 1 个(vec!)。
format_agent_picker_item_name84–103 ↗
fn format_agent_picker_item_name(
agent_nickname: Option<&str>,
agent_role: Option<&str>,
is_primary: bool,
) -> String
作用:把智能体的昵称、角色和“是否主线程”整理成选择器里好懂的名字。这样用户不会只看到一串内部 ID。
数据流:输入昵称、角色、是否主线程 → 先清理空白和空字符串 → 主线程固定显示为“Main [default]”,其他情况按“昵称 [角色]”“昵称”“[角色]”或“Agent”输出。
调用关系:它是 /agent 选择列表的命名规则中心;上层界面在生成每一行候选项时会用它,但它自己只做字符串整理。
调用图:外部调用 1 个(format!)。
previous_agent_shortcut105–107 ↗
fn previous_agent_shortcut() -> crate::key_hint::KeyBinding
作用:定义“切到上一个智能体”的标准快捷键。这里用的是 Alt+左箭头。
数据流:没有业务输入 → 调用通用按键提示工具生成 Alt+Left 的按键绑定 → 输出一个可用于显示和匹配的快捷键对象。
调用关系:它会被选择器底部提示、快捷键提示测试,以及 previous_agent_shortcut_matches 使用;后者拿它来判断用户刚按的键是不是“上一个”。
调用图:调用 1 个内部函数(alt);被 3 处调用(picker_subtitle, picker_subtitle_mentions_shortcuts, previous_agent_shortcut_matches)。
next_agent_shortcut109–111 ↗
fn next_agent_shortcut() -> crate::key_hint::KeyBinding
作用:定义“切到下一个智能体”的标准快捷键。这里用的是 Alt+右箭头。
数据流:没有业务输入 → 调用通用按键提示工具生成 Alt+Right 的按键绑定 → 输出一个可用于显示和匹配的快捷键对象。
调用关系:它会被选择器底部提示、快捷键提示测试,以及 next_agent_shortcut_matches 使用;后者拿它来判断用户刚按的键是不是“下一个”。
调用图:调用 1 个内部函数(alt);被 3 处调用(picker_subtitle, picker_subtitle_mentions_shortcuts, next_agent_shortcut_matches)。
previous_agent_shortcut_matches115–121 ↗
fn previous_agent_shortcut_matches(
key_event: KeyEvent,
allow_word_motion_fallback: bool,
) -> bool
作用:判断一次按键是不是“切到上一个智能体”。它不仅认标准的 Alt+左箭头,也可按条件接受 macOS 终端的替代按法。
数据流:输入一次键盘事件和是否允许替代按法 → 先用 previous_agent_shortcut 检查标准快捷键 → 再用 previous_agent_word_motion_fallback 检查平台兼容按法 → 输出 true 或 false。
调用关系:按键处理流程会用它决定是否触发向前切换;它把标准键定义交给 previous_agent_shortcut,把特殊终端兼容判断交给 previous_agent_word_motion_fallback。
调用图:调用 2 个内部函数(previous_agent_shortcut, previous_agent_word_motion_fallback)。
next_agent_shortcut_matches125–131 ↗
fn next_agent_shortcut_matches(
key_event: KeyEvent,
allow_word_motion_fallback: bool,
) -> bool
作用:判断一次按键是不是“切到下一个智能体”。它处理标准快捷键,也处理 macOS 某些终端发出的替代键。
数据流:输入一次键盘事件和是否允许替代按法 → 先检查 Alt+右箭头 → 再检查平台兼容按法 → 输出这次按键是否应该切到下一个智能体。
调用关系:按键处理流程会用它决定是否触发向后切换;它依赖 next_agent_shortcut 和 next_agent_word_motion_fallback 分别处理标准和兼容情况。
调用图:调用 2 个内部函数(next_agent_shortcut, next_agent_word_motion_fallback)。
previous_agent_word_motion_fallback155–160 ↗
fn previous_agent_word_motion_fallback(
_key_event: KeyEvent,
_allow_word_motion_fallback: bool,
) -> bool
作用:处理“切到上一个智能体”的平台兼容键。特别是在 macOS 上,有些终端会把 Option+左箭头发成 Option+b。
数据流:输入一次键盘事件和是否允许 fallback(备用规则) → 在允许时检查它是否像 Option+b 这样的“向前移动一个词”按键 → 输出是否匹配;非 macOS 版本总是返回 false。
调用关系:它只被 previous_agent_shortcut_matches 调用,用来补上标准 Alt+左箭头收不到时的空缺;调用方会在输入框为空等安全场景才允许它,避免影响正常编辑文字。
调用图:被 1 处调用(previous_agent_shortcut_matches);外部调用 1 个(matches!)。
next_agent_word_motion_fallback181–186 ↗
fn next_agent_word_motion_fallback(
_key_event: KeyEvent,
_allow_word_motion_fallback: bool,
) -> bool
作用:处理“切到下一个智能体”的平台兼容键。macOS 某些终端可能把 Option+右箭头发成 Option+f。
数据流:输入一次键盘事件和是否允许备用规则 → 在允许时检查它是否是 Option+f 这样的按键 → 输出是否匹配;非 macOS 版本总是返回 false。
调用关系:它只被 next_agent_shortcut_matches 调用,目的是让不同终端里的智能体切换都尽量可用,同时不打扰用户在输入框里按词移动光标。
调用图:被 1 处调用(next_agent_shortcut_matches);外部调用 1 个(matches!)。
spawn_request_summary188–201 ↗
fn spawn_request_summary(item: &ThreadItem) -> Option<SpawnRequestSummary>
作用:从“启动智能体”的事件里提取模型名和推理强度。这样历史记录里可以显示这个智能体是用什么配置启动的。
数据流:输入一条线程事件 → 如果它确实是 SpawnAgent,并且带有 model 和 reasoning_effort → 输出一个 SpawnRequestSummary;否则输出 None。
调用关系:它会被 on_collab_agent_tool_call 和 tool_call_history_cell 使用;前者可缓存启动信息,后者在渲染启动结束行时拿来补充显示。
调用图:被 2 处调用(on_collab_agent_tool_call, tool_call_history_cell)。
tool_call_history_cell203–279 ↗
fn tool_call_history_cell(
item: &ThreadItem,
cached_spawn_request: Option<&SpawnRequestSummary>,
mut agent_metadata: impl FnMut(ThreadId) -> AgentMetadata,
) -> Option<PlainHistoryCell>
作用:把一条多智能体工具调用事件变成终端历史记录里的一个可显示块。它是“机器事件”到“用户可读文字”的主要入口。
数据流:输入一条 ThreadItem、可选的启动配置缓存、以及按线程 ID 查询昵称和角色的函数 → 先确认这是不是协作智能体工具调用 → 再按工具类型和状态选择不同展示方式,例如启动完成、发送输入完成、等待开始、等待结束、关闭完成 → 输出 PlainHistoryCell;如果这条事件暂时不该显示,就输出 None。
调用关系:on_collab_agent_tool_call 会在收到协作工具调用时使用它;测试也大量调用它验证显示效果。它会把具体排版交给 spawn_end、waiting_begin、waiting_end 等小函数,也会在需要时调用 spawn_request_summary。
调用图:调用 4 个内部函数(spawn_end, spawn_request_summary, waiting_begin, waiting_end);被 4 处调用(on_collab_agent_tool_call, collab_events_snapshot, collab_resume_interrupted_snapshot, title_styles_nickname_and_role);外部调用 1 个(matches!)。
sub_agent_activity_display281–296 ↗
fn sub_agent_activity_display(item: &ThreadItem) -> Option<SubAgentActivityDisplay>
作用:从子智能体活动事件里提取界面需要记住的关键信息,比如线程 ID、路径、是否看起来还在运行。
数据流:输入一条 ThreadItem → 如果它是 SubAgentActivity,就解析 agent_thread_id,复制 agent_path,并根据事件种类判断运行提示 → 输出 SubAgentActivityDisplay;不是这类事件或 ID 无法解析时输出 None。
调用关系:它服务于上层界面对子智能体状态的刷新;它依赖 parse_thread_id 把字符串 ID 变成更可靠的 ThreadId。
调用图:调用 1 个内部函数(parse_thread_id);外部调用 1 个(matches!)。
sub_agent_activity_history_cell298–309 ↗
fn sub_agent_activity_history_cell(item: &ThreadItem) -> Option<PlainHistoryCell>
作用:把子智能体的开始、交互、打断事件变成一条历史记录。用户因此能看到子智能体什么时候被启动或中断。
数据流:输入一条 ThreadItem → 如果它是 SubAgentActivity,就用 sub_agent_activity_title 做标题,再用 collab_event 包成历史记录单元 → 输出 PlainHistoryCell;否则输出 None。
调用关系:on_sub_agent_activity 会在收到子智能体活动时调用它;它把标题生成交给 sub_agent_activity_title,把统一外观包装交给 collab_event。
调用图:调用 2 个内部函数(collab_event, sub_agent_activity_title);被 1 处调用(on_sub_agent_activity);外部调用 1 个(new)。
sub_agent_activity_summary311–317 ↗
fn sub_agent_activity_summary(kind: SubAgentActivityKind, agent_path: &str) -> String
作用:生成一行简短的子智能体活动摘要。它适合放在不需要复杂样式的地方。
数据流:输入活动类型和智能体路径 → 按 Started、Interacted、Interrupted 三种情况拼出英文短句 → 输出普通字符串。
调用关系:它是一个轻量文字转换工具,不依赖本文件其他函数;上层可在状态栏、日志或摘要位置使用。
调用图:外部调用 1 个(format!)。
sub_agent_activity_title319–329 ↗
fn sub_agent_activity_title(kind: SubAgentActivityKind, agent_path: &str) -> Line<'static>
作用:生成带样式的子智能体活动标题。它会把动作词加粗,把路径用醒目的颜色标出来。
数据流:输入活动类型和路径 → 选择“Started”“Interacted with”或“Interrupted”这样的前缀 → 拼成带颜色的文字片段 → 交给 title_spans_line 加上统一的圆点开头 → 输出一行标题。
调用关系:它被 sub_agent_activity_history_cell 调用,是子智能体历史记录标题的专用工厂;最后的统一标题格式由 title_spans_line 完成。
调用图:调用 1 个内部函数(title_spans_line);被 1 处调用(sub_agent_activity_history_cell);外部调用 1 个(vec!)。
spawn_end331–351 ↗
fn spawn_end(
new_thread_id: Option<ThreadId>,
prompt: &str,
spawn_request: Option<&SpawnRequestSummary>,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryC
作用:生成“智能体启动结束”的历史记录。成功时显示启动了谁,失败时显示启动失败。
数据流:输入新线程 ID、启动提示词、可选启动配置、以及查询智能体昵称角色的函数 → 如果有线程 ID,就取出元数据并生成“Spawned 某某”的标题;否则生成失败标题 → 如果提示词非空,就加一行截短后的提示词 → 输出 PlainHistoryCell。
调用关系:tool_call_history_cell 在 SpawnAgent 完成后调用它;它内部借助 agent_label、title_with_agent、title_text、prompt_line 和 collab_event 拼出最终显示。
调用图:调用 5 个内部函数(agent_label, collab_event, prompt_line, title_text, title_with_agent);被 1 处调用(tool_call_history_cell);外部调用 1 个(new)。
interaction_end353–369 ↗
fn interaction_end(
receiver_thread_id: ThreadId,
prompt: &str,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryCell
作用:生成“已经给某个智能体发送输入”的历史记录。用户可以回看自己给哪个智能体补充了什么要求。
数据流:输入接收方线程 ID、提示词、元数据查询函数 → 生成“Sent input to 某某”的标题 → 提示词非空时加一行预览 → 输出 PlainHistoryCell。
调用关系:它用于 SendInput 完成后的展示;它把名字格式交给 agent_label 和 title_with_agent,把提示词整理交给 prompt_line,最后由 collab_event 包装成历史记录。
调用图:调用 4 个内部函数(agent_label, collab_event, prompt_line, title_with_agent);外部调用 1 个(new)。
waiting_begin371–401 ↗
fn waiting_begin(
receiver_thread_ids: &[String],
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryCell
作用:生成“正在等待智能体”的历史记录。等待一个智能体和等待多个智能体时,它会给出不同的标题和明细。
数据流:输入一组接收方线程 ID 字符串和元数据查询函数 → 解析能识别的线程 ID,取出昵称角色 → 如果只有一个就显示等待谁,多个就显示等待几个并列出名单,解析不到就显示笼统的“Waiting for agents” → 输出 PlainHistoryCell。
调用关系:tool_call_history_cell 在 Wait 仍在进行时调用它;它会用 agent_label、title_with_agent、title_text 和 collab_event 组合出最终界面内容。
调用图:调用 4 个内部函数(agent_label, collab_event, title_text, title_with_agent);被 1 处调用(tool_call_history_cell);外部调用 2 个(new, format!)。
waiting_end403–410 ↗
fn waiting_end(
receiver_thread_ids: &[String],
agents_states: &std::collections::HashMap<String, CollabAgentState>,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainH
作用:生成“等待结束”的历史记录,并列出各智能体最后的状态。这样用户能看到谁完成了、谁报错了。
数据流:输入等待的线程 ID 列表、后端返回的状态表、元数据查询函数 → 调用 wait_complete_lines 生成每个智能体的状态明细 → 加上“Finished waiting”标题 → 输出 PlainHistoryCell。
调用关系:tool_call_history_cell 在 Wait 已完成时调用它;它把复杂的状态行整理交给 wait_complete_lines,再用 collab_event 统一包装。
调用图:调用 3 个内部函数(collab_event, title_text, wait_complete_lines);被 1 处调用(tool_call_history_cell)。
close_end412–424 ↗
fn close_end(
receiver_thread_id: ThreadId,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryCell
作用:生成“某个智能体已关闭”的历史记录。它让用户知道这个协作线程已经被结束。
数据流:输入被关闭的线程 ID 和元数据查询函数 → 找到这个智能体的显示名 → 生成“Closed 某某”的标题 → 输出没有额外明细的 PlainHistoryCell。
调用关系:它用于 CloseAgent 完成后的展示;它依赖 agent_label、title_with_agent 和 collab_event 组成最终行。
调用图:调用 3 个内部函数(agent_label, collab_event, title_with_agent);外部调用 1 个(new)。
resume_begin426–438 ↗
fn resume_begin(
receiver_thread_id: ThreadId,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryCell
作用:生成“正在恢复某个智能体”的历史记录。用户能看到系统已经开始尝试恢复一个被暂停或中断的智能体。
数据流:输入目标线程 ID 和元数据查询函数 → 取到显示名 → 生成“Resuming 某某”的标题 → 输出 PlainHistoryCell。
调用关系:它用于 ResumeAgent 进行中的展示;它通过 agent_label、title_with_agent 和 collab_event 复用统一的标题和历史记录样式。
调用图:调用 3 个内部函数(agent_label, collab_event, title_with_agent);外部调用 1 个(new)。
resume_end440–454 ↗
fn resume_end(
receiver_thread_id: ThreadId,
status: Option<&CollabAgentState>,
fallback_error: &str,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -> PlainHistoryCell
作用:生成“恢复智能体结束”的历史记录,并显示恢复后的状态或失败原因。
数据流:输入目标线程 ID、可选状态、备用错误文案、元数据查询函数 → 生成“Resumed 某某”的标题 → 用 status_summary_line 把状态变成一行说明;如果没有状态就显示备用错误 → 输出 PlainHistoryCell。
调用关系:它用于 ResumeAgent 结束后的展示;标题部分沿用 agent_label 和 title_with_agent,整体由 collab_event 包装。
调用图:调用 3 个内部函数(agent_label, collab_event, title_with_agent);外部调用 1 个(vec!)。
collab_event456–462 ↗
fn collab_event(title: Line<'static>, details: Vec<Line<'static>>) -> PlainHistoryCell
作用:把标题和明细行包装成统一样式的历史记录单元。它是本文件很多展示函数共用的“外壳”。
数据流:输入一行标题和若干明细行 → 标题放第一行 → 如果有明细,就给明细加上类似树枝的缩进前缀 → 输出 PlainHistoryCell。
调用关系:close_end、interaction_end、resume_begin、resume_end、spawn_end、sub_agent_activity_history_cell、waiting_begin、waiting_end 都会调用它;它再调用 prefix_lines 和 PlainHistoryCell::new 完成最终结构。
调用图:调用 2 个内部函数(new, prefix_lines);被 8 处调用(close_end, interaction_end, resume_begin, resume_end, spawn_end, sub_agent_activity_history_cell, waiting_begin, waiting_end);外部调用 1 个(vec!)。
title_text464–466 ↗
fn title_text(title: impl Into<String>) -> Line<'static>
作用:生成一个只有纯标题文字的标题行。适合“Finished waiting”“Agent spawn failed”这种不需要带智能体名字的标题。
数据流:输入任意能变成字符串的标题 → 把它变成加粗文字片段 → 交给 title_spans_line 加统一圆点 → 输出一行标题。
调用关系:spawn_end、waiting_begin、waiting_end 会在需要简单标题时调用它;它把统一标题前缀交给 title_spans_line。
调用图:调用 1 个内部函数(title_spans_line);被 3 处调用(spawn_end, waiting_begin, waiting_end);外部调用 1 个(vec!)。
title_with_agent468–477 ↗
fn title_with_agent(
prefix: &str,
agent: AgentLabel<'_>,
spawn_request: Option<&SpawnRequestSummary>,
) -> Line<'static>
作用:生成“动作 + 智能体 + 可选启动配置”的标题行。比如“Spawned Robie [explorer] (gpt-5 high)”。
数据流:输入动作前缀、智能体标签、可选启动配置 → 先放加粗动作词 → 加上智能体名字和角色 → 如果有模型和推理强度,再追加配置说明 → 输出统一格式标题行。
调用关系:close_end、interaction_end、resume_begin、resume_end、spawn_end、waiting_begin 都会用它;它把名字部分交给 agent_label_spans,把启动配置交给 spawn_request_spans,把最终行交给 title_spans_line。
调用图:调用 3 个内部函数(agent_label_spans, spawn_request_spans, title_spans_line);被 6 处调用(close_end, interaction_end, resume_begin, resume_end, spawn_end, waiting_begin);外部调用 1 个(vec!)。
title_spans_line479–484 ↗
fn title_spans_line(mut spans: Vec<Span<'static>>) -> Line<'static>
作用:给标题文字片段加上统一的开头圆点。这样所有协作事件看起来像同一类记录。
数据流:输入一组标题文字片段 → 在最前面插入一个灰色的“• ” → 输出一整行 Line。
调用关系:sub_agent_activity_title、title_text、title_with_agent 都会调用它;它是标题视觉风格的最后一道统一处理。
调用图:被 3 处调用(sub_agent_activity_title, title_text, title_with_agent);外部调用 2 个(from, with_capacity)。
parse_thread_id486–488 ↗
fn parse_thread_id(thread_id: &str) -> Option<ThreadId>
作用:把字符串形式的线程 ID 转成程序内部使用的 ThreadId。这样后续比较和显示更可靠。
数据流:输入一个字符串 → 调用 ThreadId 的解析方法 → 成功时输出 ThreadId,失败时输出 None。
调用关系:sub_agent_activity_display 会调用它来解析子智能体线程 ID;文件里其他流程也按同样思路过滤掉无法识别的 ID。
调用图:调用 1 个内部函数(from_string);被 1 处调用(sub_agent_activity_display)。
agent_label490–496 ↗
fn agent_label(thread_id: ThreadId, metadata: &AgentMetadata) -> AgentLabel<'_>
作用:把线程 ID 和元数据打包成一个临时的“智能体标签”。后面的显示函数会用它决定展示昵称、角色还是 ID。
数据流:输入线程 ID 和 AgentMetadata → 从元数据里借出昵称和角色 → 输出 AgentLabel,里面包含线程 ID、昵称、角色三个线索。
调用关系:close_end、interaction_end、resume_begin、resume_end、spawn_end、waiting_begin 都会先用它准备标签,再交给标题或明细渲染函数。
调用图:被 6 处调用(close_end, interaction_end, resume_begin, resume_end, spawn_end, waiting_begin)。
agent_label_line498–500 ↗
fn agent_label_line(agent: AgentLabel<'_>) -> Line<'static>
作用:把一个智能体标签变成单独一行。等待多个智能体时,可以用它列出每个被等待的人。
数据流:输入 AgentLabel → 调用 agent_label_spans 生成带样式的文字片段 → 输出一行 Line。
调用关系:它是 agent_label_spans 的薄包装,适合需要整行而不是片段列表的地方。
调用图:调用 1 个内部函数(agent_label_spans)。
agent_label_spans502–524 ↗
fn agent_label_spans(agent: AgentLabel<'_>) -> Vec<Span<'static>>
作用:把智能体标签变成可上色的文字片段。优先显示昵称,其次显示线程 ID,最后才显示泛泛的“agent”。
数据流:输入 AgentLabel → 清理昵称和角色里的空白 → 有昵称就用青色加粗显示昵称;没有昵称但有线程 ID 就显示 ID;都没有就显示“agent” → 如果有角色,再追加“[角色]” → 输出文字片段列表。
调用关系:agent_label_line 和 title_with_agent 会调用它;它是智能体名字在整个文件里保持一致显示的核心函数。
调用图:被 2 处调用(agent_label_line, title_with_agent);外部调用 3 个(from, new, format!)。
spawn_request_spans526–543 ↗
fn spawn_request_spans(spawn_request: Option<&SpawnRequestSummary>) -> Vec<Span<'static>>
作用:把启动智能体时的模型和推理强度变成标题里的补充说明。比如显示成“(gpt-5 high)”。
数据流:输入可选 SpawnRequestSummary → 没有配置就输出空列表 → 如果模型为空且推理强度是默认值,也不显示 → 否则拼出括号说明并标成醒目颜色 → 输出文字片段列表。
调用关系:title_with_agent 会调用它;它只负责启动配置那一小段文字,不负责整条标题。
调用图:被 1 处调用(title_with_agent);外部调用 4 个(default, new, format!, vec!)。
prompt_line545–555 ↗
fn prompt_line(prompt: &str) -> Option<Line<'static>>
作用:把用户发给智能体的提示词整理成一行预览。太长的内容会被截短,避免历史记录难读。
数据流:输入提示词字符串 → 去掉前后空白 → 如果为空就输出 None → 如果非空就按固定长度截断,再包装成一行 Line → 输出可选行。
调用关系:interaction_end 和 spawn_end 会调用它来显示提示词明细;它依赖 truncate_text 处理“别太长”的规则。
调用图:调用 1 个内部函数(truncate_text);被 2 处调用(interaction_end, spawn_end);外部调用 2 个(from, from)。
wait_complete_lines557–597 ↗
fn wait_complete_lines(
receiver_thread_ids: &[String],
agents_states: &std::collections::HashMap<String, CollabAgentState>,
agent_metadata: &mut impl FnMut(ThreadId) -> AgentMetadata,
) -
作用:整理“等待完成”时每个智能体的状态明细。它会尽量按等待列表显示,同时也补上状态表里额外出现的智能体。
数据流:输入等待的线程 ID 列表、状态表、元数据查询函数 → 先按等待列表解析并匹配状态 → 记录已经出现过的线程 → 再把状态表里未出现的额外线程按 ID 排序补进来 → 如果没有任何结果,输出“No agents completed yet”;否则输出每个智能体加状态摘要的一行。
调用关系:waiting_end 调用它生成明细行;它内部使用 agent_label_spans 和 status_summary_spans 让名字和状态都保持统一样式。
调用图:被 1 处调用(waiting_end);外部调用 2 个(new, vec!)。
first_agent_state599–612 ↗
fn first_agent_state(
receiver_thread_ids: &[String],
agents_states: &'a std::collections::HashMap<String, CollabAgentState>,
) -> Option<&'a CollabAgentState>
作用:从一堆智能体状态里挑一个最适合代表当前结果的状态。恢复智能体结束时,如果有目标线程就优先用目标线程的状态。
数据流:输入接收方线程 ID 列表和状态表 → 先按接收方顺序找第一个有状态的线程 → 如果找不到,就按字符串 ID 取最小的那条状态 → 输出可选状态引用。
调用关系:它是 ResumeAgent 结束展示里的辅助选择器;选出的状态会被后续状态摘要函数变成用户能读的文字。
status_summary_line614–619 ↗
fn status_summary_line(status: Option<&CollabAgentState>, fallback_error: &str) -> Line<'static>
作用:把一个可选的智能体状态变成一整行说明。没有状态时,它会显示备用错误。
数据流:输入可选状态和备用错误文案 → 有状态就交给 status_summary_spans → 没有状态就交给 error_summary_spans → 输出一行 Line。
调用关系:resume_end 会用它显示恢复后的结果;它把具体的状态和错误文字样式分别交给 status_summary_spans、error_summary_spans。
调用图:调用 2 个内部函数(error_summary_spans, status_summary_spans)。
status_summary_spans621–648 ↗
fn status_summary_spans(status: &CollabAgentState) -> Vec<Span<'static>>
作用:把智能体状态翻译成带颜色的短说明。比如运行中、已完成、报错、未找到等。
数据流:输入 CollabAgentState → 按状态枚举选择不同文字和颜色 → 完成状态会附带截短后的回复预览;错误状态会转交 error_summary_spans → 输出文字片段列表。
调用关系:status_summary_line 会调用它;等待完成明细也会用同样的状态摘要规则,保证不同地方看到的状态说法一致。
调用图:调用 2 个内部函数(error_summary_spans, truncate_text);被 1 处调用(status_summary_line);外部调用 2 个(from, vec!)。
error_summary_spans650–661 ↗
fn error_summary_spans(error: &str) -> Vec<Span<'static>>
作用:把错误信息整理成短短一段红色提示。这样错误醒目,但不会因为太长挤爆界面。
数据流:输入错误字符串 → 先生成红色“Error” → 把错误内容压成单空格文本并截短 → 如果还有内容,就追加“ - 预览” → 输出文字片段列表。
调用关系:status_summary_line 在没有状态时会调用它;status_summary_spans 遇到 Errored 状态时也会调用它。
调用图:调用 1 个内部函数(truncate_text);被 2 处调用(status_summary_line, status_summary_spans);外部调用 2 个(from, vec!)。
tests::collab_events_snapshot678–795 ↗
fn collab_events_snapshot()
作用:用快照测试检查多种协作事件渲染出来的文字是否稳定。快照测试就是把输出保存成样本,以后改代码时对比有没有变。
数据流:测试先构造几个固定线程 ID 和多条事件,包括启动、发送输入、等待、等待完成、关闭 → 分别调用 tool_call_history_cell 转成历史记录 → 再把显示文本拼起来和快照比对。
调用关系:它直接验证 tool_call_history_cell 及其背后一串渲染函数;同时使用 agent_state、metadata_for、cell_to_text 等测试辅助函数准备数据和提取文字。
调用图:调用 2 个内部函数(from_string, tool_call_history_cell);外部调用 5 个(from, new, assert_snapshot!, agent_state, vec!)。
tests::agent_shortcut_matches_option_arrow_word_motion_fallbacks_only_when_allowed799–824 ↗
fn agent_shortcut_matches_option_arrow_word_motion_fallbacks_only_when_allowed()
作用:在 macOS 上测试智能体切换快捷键的兼容规则。它确认 Option+b/f 只有在明确允许时才会被当成切换智能体。
数据流:测试输入多组模拟按键事件和 allow_word_motion_fallback 开关 → 调用 previous_agent_shortcut_matches、next_agent_shortcut_matches → 用断言确认标准方向键总能匹配,备用键只在允许时匹配。
调用关系:它保护 previous_agent_shortcut_matches、next_agent_shortcut_matches 以及两个 word_motion_fallback 函数的行为,避免兼容逻辑误伤正常文本编辑。
调用图:外部调用 1 个(assert!)。
tests::agent_shortcut_matches_option_arrows_only828–845 ↗
fn agent_shortcut_matches_option_arrows_only()
作用:在非 macOS 平台测试智能体切换只认 Alt+方向键。这样不同系统的备用规则不会串台。
数据流:测试构造 Alt+左、Alt+右、Alt+b、Alt+f 等按键 → 调用快捷键匹配函数 → 断言只有方向键会匹配,字母备用键不会匹配。
调用关系:它保护非 macOS 版本的 previous_agent_word_motion_fallback 和 next_agent_word_motion_fallback 始终返回 false 的设计。
调用图:外部调用 1 个(assert!)。
tests::title_styles_nickname_and_role848–883 ↗
fn title_styles_nickname_and_role()
作用:检查标题里昵称、角色和启动配置的文字内容与样式是否正确。它不只看文字,还看颜色和加粗效果。
数据流:测试构造一条启动智能体事件 → 调用 tool_call_history_cell 生成历史记录 → 取出第一行标题 → 断言昵称是青色加粗、角色格式正确、模型配置是紫色。
调用关系:它验证 tool_call_history_cell、title_with_agent、agent_label_spans、spawn_request_spans 这些函数配合后的最终显示效果。
调用图:调用 2 个内部函数(from_string, tool_call_history_cell);外部调用 6 个(from, new, assert!, assert_eq!, agent_state, vec!)。
tests::collab_resume_interrupted_snapshot886–913 ↗
fn collab_resume_interrupted_snapshot()
作用:测试“恢复智能体后发现它被中断”的历史记录显示。它确保这种边界状态的文案不会意外变化。
数据流:测试构造一条 ResumeAgent 完成事件,状态是 Interrupted → 调用 tool_call_history_cell → 把结果转成文本并和快照比对。
调用关系:它覆盖 ResumeAgent 分支,间接验证 first_agent_state、resume_end、status_summary_line、status_summary_spans 的配合。
调用图:调用 2 个内部函数(from_string, tool_call_history_cell);外部调用 4 个(from, assert_snapshot!, agent_state, vec!)。
tests::agent_state915–920 ↗
fn agent_state(status: CollabAgentStatus, message: Option<&str>) -> CollabAgentState
作用:测试里用来快速造一个智能体状态对象。这样每个测试不用重复写结构体字段。
数据流:输入状态枚举和可选消息字符串 → 把消息转成 owned 字符串 → 输出 CollabAgentState。
调用关系:collab_events_snapshot、title_styles_nickname_and_role、collab_resume_interrupted_snapshot 都用它准备测试事件里的状态数据。
tests::metadata_for922–936 ↗
fn metadata_for(thread_id: ThreadId, robie_id: ThreadId, bob_id: ThreadId) -> AgentMetadata
作用:测试里按线程 ID 返回固定的昵称和角色。它让测试能验证“Robie [explorer]”“Bob [worker]”这类显示。
数据流:输入一个线程 ID,以及 Robie 和 Bob 的已知 ID → 如果匹配 Robie 就返回 Robie 的元数据,匹配 Bob 就返回 Bob 的元数据,否则返回空元数据 → 输出 AgentMetadata。
调用关系:多个渲染测试把它作为回调传给 tool_call_history_cell;这样生产代码以为自己在查询真实元数据,测试则得到可预测结果。
调用图:外部调用 1 个(default)。
tests::cell_to_text938–944 ↗
fn cell_to_text(cell: &PlainHistoryCell) -> String
作用:把一个历史记录单元转成纯文本,方便做快照对比。样式颜色不在这里保留,只看最终文字。
数据流:输入 PlainHistoryCell → 按固定宽度取出显示行 → 每行交给 line_to_text 去掉样式 → 用换行拼成字符串 → 输出纯文本。
调用关系:快照测试会用它把 tool_call_history_cell 的结果变成可比较文本;它把单行处理交给 line_to_text。
调用图:调用 1 个内部函数(display_lines)。
tests::line_to_text946–952 ↗
fn line_to_text(line: &Line<'static>) -> String
作用:把一行带样式的终端文字变成普通字符串。它用于测试时忽略颜色,只比较可见文字。
数据流:输入一行 Line → 遍历里面的每个 Span,取出文字内容 → 拼接成一个字符串 → 输出纯文本行。
调用关系:cell_to_text 会逐行调用它;它是快照测试文本提取的最底层小工具。
tui/src/pager_overlay.rs源码 ↗
这个文件解决的是“终端窗口太小,内容太长,看不全”的问题。它做了一个类似阅读器的覆盖层:上面有标题,中间显示内容,下面显示滚动比例和快捷键。核心零件是 PagerView,它只关心怎么把一串可绘制内容排好、滚动好、画到屏幕上。TranscriptOverlay 在它上面加了聊天记录能力:显示已经确定的历史消息,还能在末尾接上一段正在生成的“实时尾巴”。为了避免每次刷新都重新排版,它会用缓存记住高度和实时尾巴的版本。StaticOverlay 则用于固定文本页面。文件里还特别处理了链接标记、用户消息高亮、滚动到底时自动跟随新内容等细节;没有这些,聊天记录页会卡、链接会丢、用户滚动位置也容易被新消息打乱。
Overlay::new_transcript59–61 ↗
fn new_transcript(cells: Vec<Arc<dyn HistoryCell>>, keymap: PagerKeymap) -> Self
作用:创建一个聊天记录覆盖层,用来显示完整对话历史。用户打开 transcript 页面时会用到它。
数据流:进去的是历史消息列表和翻页按键配置 → 它把这些交给 TranscriptOverlay 创建真正的记录页 → 出来的是 Overlay 这个统一外壳里的 Transcript 版本。
调用关系:它是外部打开记录页时的入口之一;调用 TranscriptOverlay::new 做具体搭建,然后让上层只需要拿 Overlay 来统一处理事件。
调用图:调用 1 个内部函数(new);被 5 处调用(handle_key_event, clear_only_ui_reset_preserves_chat_session_state, queued_rollback_syncs_overlay_and_clears_deferred_history, open_transcript_overlay, open_pending_transcript_if_ready);外部调用 1 个(Transcript)。
Overlay::new_static_with_lines63–69 ↗
fn new_static_with_lines(
lines: Vec<Line<'static>>,
title: String,
keymap: PagerKeymap,
) -> Self
作用:创建一个显示固定文本行的覆盖层,比如帮助信息或说明文字。它适合内容已经是一行一行文本的场景。
数据流:进去的是文本行、标题和按键配置 → 它调用 StaticOverlay::with_title 包成可滚动页面 → 出来的是 Overlay 的 Static 版本。
调用关系:当应用需要弹出静态阅读页时会走这里;它把具体页面构造交给 StaticOverlay。
调用图:调用 1 个内部函数(with_title);被 1 处调用(handle_event);外部调用 1 个(Static)。
Overlay::new_static_with_renderables71–77 ↗
fn new_static_with_renderables(
renderables: Vec<Box<dyn Renderable>>,
title: String,
keymap: PagerKeymap,
) -> Self
作用:创建一个显示自定义可绘制内容的静态覆盖层。比纯文本更灵活,因为每块内容可以自己决定怎么画。
数据流:进去的是一组可绘制块、标题和按键配置 → 它调用 StaticOverlay::with_renderables → 出来的是统一 Overlay 外壳。
调用关系:它服务于已经准备好 Renderable 内容的调用方;后续事件处理仍由 Overlay::handle_event 分发。
调用图:调用 1 个内部函数(with_renderables);被 1 处调用(handle_event);外部调用 1 个(Static)。
Overlay::handle_event79–84 ↗
fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()>
作用:把键盘、重绘、窗口变化等事件转交给当前真正的覆盖层。这样外部不必关心现在打开的是记录页还是静态页。
数据流:进去的是一个 TUI 对象和一个事件 → 它看 Overlay 里装的是 Transcript 还是 Static → 把事件交给对应对象处理,并返回是否成功。
调用关系:它是覆盖层运行时的统一事件入口;上层主循环调用它,它再分派给 TranscriptOverlay::handle_event 或 StaticOverlay::handle_event。
Overlay::is_done86–91 ↗
fn is_done(&self) -> bool
作用:告诉外部这个覆盖层是不是该关闭了。比如用户按了退出键以后,这里会返回真。
数据流:进去的是当前 Overlay 状态 → 它询问内部具体覆盖层的完成标记 → 出来的是一个布尔值。
调用关系:主界面会用它判断是否移除覆盖层;它只是统一转问 TranscriptOverlay 或 StaticOverlay。
first_or_empty94–96 ↗
fn first_or_empty(bindings: &[KeyBinding]) -> Vec<KeyBinding>
作用:从一组快捷键里取第一个来显示提示;如果没有快捷键,就返回空列表。它让底部提示不会因为缺配置而出错。
数据流:进去的是快捷键数组 → 它尝试拿第一个并复制出来 → 出来的是包含一个键或为空的列表。
调用关系:两个覆盖层的 render_hints 都用它准备底部提示;它把复杂按键列表压缩成适合展示的简短形式。
调用图:被 2 处调用(render_hints, render_hints);外部调用 1 个(first)。
render_key_hints99–117 ↗
fn render_key_hints(area: Rect, buf: &mut Buffer, pairs: &[(Vec<KeyBinding>, &str)])
作用:把“按什么键做什么事”的提示画成一行。用户看到的底部快捷键说明主要由它生成。
数据流:进去的是屏幕区域、缓冲区和若干组键名加说明 → 它拼出带间隔的文字片段并变暗显示 → 结果写进终端绘图缓冲区。
调用关系:TranscriptOverlay::render_hints 和 StaticOverlay::render_hints 都调用它;它是底部提示栏的公共画笔。
调用图:被 2 处调用(render_hints, render_hints);外部调用 3 个(new, from, vec!)。
PagerView::new132–147 ↗
fn new(
renderables: Vec<Box<dyn Renderable>>,
title: String,
scroll_offset: usize,
keymap: PagerKeymap,
) -> Self
作用:创建一个通用翻页视图。它保存要显示的内容、标题、当前滚动位置和按键规则。
数据流:进去的是可绘制内容列表、标题、初始滚动位置和按键配置 → 它组装成 PagerView 状态 → 出来的是一个可渲染、可滚动的阅读器。
调用关系:TranscriptOverlay 和 StaticOverlay 都靠它承载正文显示;测试工具 pager_view 也用它直接构造视图。
调用图:被 3 处调用(with_renderables, new, pager_view)。
PagerView::content_height149–154 ↗
fn content_height(&self, width: u16) -> usize
作用:计算所有内容在某个宽度下总共需要多少行。窗口变窄会换行,所以高度必须按宽度重新算。
数据流:进去的是屏幕宽度 → 它逐个询问每个内容块需要的高度并相加 → 出来的是总行数。
调用关系:PagerView::render 在绘制前调用它,用来限制滚动范围和计算底部百分比。
调用图:被 1 处调用(render)。
PagerView::render156–175 ↗
fn render(&mut self, area: Rect, buf: &mut Buffer)
作用:把整个翻页页画出来,包括标题、正文、滚动裁剪和底部进度条。它是 PagerView 的主绘制函数。
数据流:进去的是可画区域和缓冲区 → 它清屏、画标题、算正文区域和内容高度、处理待定位内容、修正滚动偏移、画正文和底栏 → 缓冲区变成完整页面画面。
调用关系:TranscriptOverlay::render 和 StaticOverlay::render 都调用它;它内部串起 render_header、render_content、render_bottom_bar 等零件。
调用图:调用 7 个内部函数(content_area, content_height, ensure_chunk_visible, render_bottom_bar, render_content, render_header, update_last_content_height);被 2 处调用(render, render)。
PagerView::render_header177–183 ↗
PagerView::render_content185–219 ↗
fn render_content(&self, area: Rect, buf: &mut Buffer)
作用:按当前滚动位置画正文,只画用户现在能看到的那一段。内容不够铺满时,它会用波浪号和空格填下面的空白。
数据流:进去的是正文区域和缓冲区 → 它根据 scroll_offset 跳过上方内容、裁剪部分露出的内容块、逐块绘制 → 最后补齐空白区域。
调用关系:由 PagerView::render 调用;当某个内容块顶部已经被滚走一部分时,它会把活交给 render_offset_content。
调用图:调用 1 个内部函数(render_offset_content);被 1 处调用(render);外部调用 4 个(from, bottom, new, right)。
PagerView::render_bottom_bar221–251 ↗
fn render_bottom_bar(
&self,
full_area: Rect,
content_area: Rect,
buf: &mut Buffer,
total_len: usize,
)
作用:画底部横线和滚动百分比。用户可以知道自己大概看到了全文的哪个位置。
数据流:进去的是总区域、正文区域、缓冲区和总内容高度 → 它计算最大滚动距离和当前百分比 → 把分隔线和百分比写到底部。
调用关系:由 PagerView::render 在正文之后调用,是翻页页面的状态提示。
PagerView::handle_key_event253–292 ↗
fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()>
作用:处理滚动相关按键,比如上下一行、翻页、半页、跳到顶部或底部。它让阅读器真正能被键盘控制。
数据流:进去的是 TUI 对象和一个按键事件 → 它按 keymap 判断是哪种滚动命令并修改 scroll_offset → 如果发生滚动,就请求下一帧重画。
调用关系:TranscriptOverlay::handle_event 和 StaticOverlay::handle_event 把非关闭键交给它;它需要 page_height 和 content_area 来算翻多少。
调用图:调用 2 个内部函数(content_area, page_height);被 2 处调用(handle_event, handle_event);外部调用 1 个(frame_requester)。
PagerView::page_height299–302 ↗
fn page_height(&self, viewport_area: Rect) -> usize
作用:告诉翻页键一次该移动多少行。它优先使用上次实际绘制时的正文高度,避免页翻得不准。
数据流:进去的是终端可视区域 → 它先看是否记过上次正文高度,没有就按当前区域估算 → 出来的是一页的行数。
调用关系:PagerView::handle_key_event 在 PageUp 和 PageDown 时调用它。
调用图:被 1 处调用(handle_key_event)。
PagerView::update_last_content_height304–306 ↗
fn update_last_content_height(&mut self, height: u16)
作用:记录最近一次正文区域有多高。这个值会影响之后翻页是否连续。
数据流:进去的是高度 → 它保存到 last_content_height → 之后 page_height 和底部判断可复用这个信息。
调用关系:PagerView::render 每次绘制时调用它,为后续按键处理提供真实布局数据。
调用图:被 1 处调用(render)。
PagerView::content_area308–313 ↗
fn content_area(&self, area: Rect) -> Rect
作用:从整个页面区域里切出正文区域。它会给顶部标题和底部栏留出空间。
数据流:进去的是完整矩形区域 → 它把 y 往下挪一行,并把高度减两行 → 出来的是正文可用区域。
调用关系:PagerView::render 和 PagerView::handle_key_event 都用它,保证绘制和滚动计算用的是同一块地方。
调用图:被 2 处调用(handle_key_event, render)。
PagerView::is_scrolled_to_bottom317–335 ↗
fn is_scrolled_to_bottom(&self) -> bool
作用:判断用户当前是不是在内容底部。这个判断关系到新内容来了以后要不要自动跟着滚下去。
数据流:进去的是当前滚动状态、上次正文高度和内容总高度 → 它处理空内容、内容不满屏、特殊的“跳到底”值等情况 → 出来是真或假。
调用关系:TranscriptOverlay 在插入、替换、合并和同步实时尾巴前都会问它;这样能保护用户手动滚上去看的位置。
调用图:被 5 处调用(consolidate_cells, insert_cell, is_scrolled_to_bottom, replace_cells, sync_live_tail)。
PagerView::scroll_chunk_into_view338–340 ↗
fn scroll_chunk_into_view(&mut self, chunk_index: usize)
作用:请求下次绘制时把某个内容块滚到可见范围内。常用于高亮某条消息后自动把它带到屏幕里。
数据流:进去的是内容块序号 → 它暂存为 pending_scroll_chunk → 下次 render 时再按实际宽度和换行情况调整滚动。
调用关系:TranscriptOverlay::set_highlight_cell 调用它;真正执行滚动的是 PagerView::ensure_chunk_visible。
调用图:被 1 处调用(set_highlight_cell)。
PagerView::ensure_chunk_visible342–360 ↗
fn ensure_chunk_visible(&mut self, idx: usize, area: Rect)
作用:确保指定内容块在当前正文区域里能看见。它会根据内容块上下边界调整滚动位置。
数据流:进去的是内容块序号和正文区域 → 它计算该块之前的高度、该块自己的高度、当前可视范围 → 必要时改动 scroll_offset。
调用关系:PagerView::render 在发现有 pending_scroll_chunk 时调用它;这样滚动计算使用最新宽度下的实际换行高度。
调用图:被 1 处调用(render)。
CachedRenderable::new371–377 ↗
fn new(renderable: impl Into<Box<dyn Renderable>>) -> Self
作用:把一个可绘制内容包起来,加上高度缓存。这样反复绘制时不用每次都重新算高度。
数据流:进去的是一个可转换成 Renderable 的内容 → 它保存原内容,并把缓存高度和上次宽度置空 → 出来的是 CachedRenderable。
调用关系:记录单元、实时尾巴、静态文本都会被它包住;调用方通常是 TranscriptOverlay::render_cells、TranscriptOverlay::live_tail_renderable 和 StaticOverlay::with_title。
调用图:被 1 处调用(live_tail_renderable);外部调用 2 个(into, new)。
CachedRenderable::render381–383 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:实际绘制缓存包装里的原始内容。缓存只管高度,不改变内容怎么画。
数据流:进去的是绘制区域和缓冲区 → 它直接调用内部 renderable 的 render → 结果写入同一个缓冲区。
调用关系:PagerView 绘制内容块时会通过 Renderable 接口间接调用它。
CachedRenderable::desired_height384–391 ↗
fn desired_height(&self, width: u16) -> u16
作用:返回内容在指定宽度下需要的高度,并尽量复用上次结果。宽度没变时就不重新计算。
数据流:进去的是宽度 → 如果宽度和上次不同,它询问内部内容高度并缓存;如果相同,就取缓存 → 出来的是高度。
调用关系:PagerView 计算总高度、裁剪内容和滚动时都会间接用它;这是减少重复排版开销的关键。
CellRenderable::render400–407 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把一条历史消息画到 transcript 页面里,并保留其中的终端超链接。超链接是可点击或可识别的网址标记。
数据流:进去的是区域和缓冲区 → 它向 HistoryCell 要带链接信息的文本行,转换成可见文字,用指定样式绘制,再把链接标记写回缓冲区 → 屏幕上既有文字也有链接信息。
调用关系:TranscriptOverlay::render_cells 会为每个历史消息创建它;PagerView 最终通过 Renderable 接口调用它。
调用图:调用 2 个内部函数(mark_buffer_hyperlinks, visible_lines);外部调用 2 个(new, from)。
CellRenderable::desired_height409–411 ↗
fn desired_height(&self, width: u16) -> u16
作用:询问这条历史消息在某个宽度下会占多少行。它用于滚动和分页计算。
数据流:进去的是宽度 → 它转问 HistoryCell 的 transcript 高度 → 出来的是行数。
调用关系:被 PagerView 通过 Renderable 接口调用;通常外面还套了一层 CachedRenderable 来缓存结果。
HyperlinkLinesRenderable::render419–424 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:绘制一组已经带有链接信息的文本行。它主要用于正在生成的实时尾巴。
数据流:进去的是区域和缓冲区 → 它把带链接行变成可见文字画出来,再把链接位置标到缓冲区 → 输出可见文字和可保留的链接。
调用关系:TranscriptOverlay::live_tail_renderable 会创建它;PagerView 负责在页面底部把它画出来。
调用图:调用 2 个内部函数(mark_buffer_hyperlinks, visible_lines);外部调用 2 个(new, from)。
HyperlinkLinesRenderable::desired_height426–432 ↗
fn desired_height(&self, width: u16) -> u16
作用:计算带链接文本在某个宽度下换行后需要多少行。这样实时尾巴也能参与滚动计算。
数据流:进去的是宽度 → 它把链接行转成可见文字,交给段落排版计算行数 → 出来的是高度,转换失败时用 0。
调用关系:PagerView 通过 Renderable 接口调用它;外层通常由 CachedRenderable 缓存高度。
调用图:调用 1 个内部函数(visible_lines);外部调用 2 个(new, from)。
TranscriptOverlay::new469–482 ↗
fn new(transcript_cells: Vec<Arc<dyn HistoryCell>>, keymap: PagerKeymap) -> Self
作用:创建聊天记录覆盖层。它一开始显示已提交的历史消息,并默认滚到底部。
数据流:进去的是历史消息和按键配置 → 它把消息转换成可绘制块,创建标题为 TRANSCRIPT 的 PagerView,设置内部状态 → 出来的是 TranscriptOverlay。
调用关系:Overlay::new_transcript 和测试辅助 transcript_overlay 会调用它;之后由 App 在绘制和事件处理中继续驱动。
调用图:调用 1 个内部函数(new);被 2 处调用(new_transcript, transcript_overlay);外部调用 1 个(render_cells)。
TranscriptOverlay::render_cells484–520 ↗
fn render_cells(
cells: &[Arc<dyn HistoryCell>],
highlight_cell: Option<usize>,
) -> Vec<Box<dyn Renderable>>
作用:把一组历史消息转换成 PagerView 能显示的内容块。它还会处理用户消息样式、高亮和消息之间的空行。
数据流:进去的是历史消息列表和可选高亮序号 → 它逐条包装成 CellRenderable 和 CachedRenderable,必要时加顶部缩进 → 出来的是可绘制块列表。
调用关系:TranscriptOverlay::new、rebuild_renderables、insert_cell 等都依赖它重建正文;这是历史数据变成屏幕内容的转换点。
TranscriptOverlay::insert_cell532–560 ↗
fn insert_cell(&mut self, cell: Arc<dyn HistoryCell>)
作用:在记录页里加入一条新的已完成历史消息,同时尽量保留实时尾巴和滚动位置。新消息到来时会用到它。
数据流:进去的是一条历史消息 → 它先判断是否在底部,临时取下实时尾巴,追加消息并重建历史内容,再把尾巴接回去 → 如果原本在底部,就继续保持到底部。
调用关系:上层 App 在 transcript 打开期间有新提交消息时会调用它;它内部用 PagerView::is_scrolled_to_bottom、take_live_tail_renderable 和 render_cells。
调用图:调用 4 个内部函数(is_scrolled_to_bottom, take_live_tail_renderable, tlbr, new);外部调用 2 个(new, render_cells)。
TranscriptOverlay::replace_cells567–580 ↗
fn replace_cells(&mut self, cells: Vec<Arc<dyn HistoryCell>>)
作用:整体替换已提交的历史消息列表,同时保留正在显示的实时尾巴。比如回滚或裁剪历史后需要同步页面。
数据流:进去的是新的历史消息列表 → 它记录是否跟随底部,替换 cells,清掉越界高亮,重建可绘制内容 → 必要时继续滚到底部。
调用关系:当主聊天记录发生批量变化时会用它;具体重建交给 rebuild_renderables。
调用图:调用 2 个内部函数(is_scrolled_to_bottom, rebuild_renderables)。
TranscriptOverlay::consolidate_cells589–623 ↗
fn consolidate_cells(
&mut self,
range: std::ops::Range<usize>,
consolidated: Arc<dyn HistoryCell>,
)
作用:把一段历史消息合并成一条显示,保持 transcript 页面和主记录一致。它还会修正高亮位置。
数据流:进去的是要合并的范围和合并后的消息 → 它先把范围夹到现有长度内,调整高亮序号,替换 cells 中那一段,重建显示 → 如果之前在底部则继续到底。
调用关系:主流程把多条 agent 消息合并时会调用它;它保护 overlay 即使消息数量不同步也不崩。
调用图:调用 2 个内部函数(is_scrolled_to_bottom, rebuild_renderables);外部调用 1 个(once)。
TranscriptOverlay::sync_live_tail637–671 ↗
fn sync_live_tail(
&mut self,
width: u16,
active_key: Option<ActiveCellTranscriptKey>,
compute_lines: impl FnOnce(u16) -> Option<Vec<HyperlinkLine>>,
)
作用:同步 transcript 底部那段“还没提交、正在生成”的内容。它只在版本键变化时重算,避免每帧都做昂贵排版。
数据流:进去的是宽度、活动消息版本信息和一个生成文本行的函数 → 它组成缓存键,若没变就直接返回;若变了,取下旧尾巴,按需计算新尾巴并接到末尾 → 保持原本的跟随底部行为。
调用关系:App 在绘制 TranscriptOverlay 时调用它;它把实时内容包装交给 live_tail_renderable,并依赖 take_live_tail_renderable 管理末尾缓存块。
调用图:调用 2 个内部函数(is_scrolled_to_bottom, take_live_tail_renderable);外部调用 1 个(live_tail_renderable)。
TranscriptOverlay::set_highlight_cell673–679 ↗
fn set_highlight_cell(&mut self, cell: Option<usize>)
作用:设置哪条历史消息被高亮,并请求把它滚到可见范围。编辑上一条或下一条消息时会用到。
数据流:进去的是可选消息序号 → 它保存高亮状态,重建可绘制内容 → 如果有高亮,就通知 PagerView 下次把那块滚出来。
调用关系:外部编辑导航会调用它;它内部调用 rebuild_renderables 和 PagerView::scroll_chunk_into_view。
调用图:调用 2 个内部函数(scroll_chunk_into_view, rebuild_renderables)。
TranscriptOverlay::is_scrolled_to_bottom685–687 ↗
fn is_scrolled_to_bottom(&self) -> bool
作用:告诉外部 transcript 当前是否停在底部。外部可以据此决定是否继续驱动看得见的动画。
数据流:进去的是 overlay 当前状态 → 它转问内部 PagerView → 出来的是是否到底部。
调用关系:App 绘制循环会用它判断实时尾巴动画是否值得刷新;它只是 PagerView::is_scrolled_to_bottom 的公开包装。
调用图:调用 1 个内部函数(is_scrolled_to_bottom)。
TranscriptOverlay::rebuild_renderables689–695 ↗
fn rebuild_renderables(&mut self)
作用:重新生成 transcript 的所有可绘制内容,同时保留末尾实时尾巴。历史数据或高亮变化后需要它。
数据流:进去的是当前 cells、高亮和可能存在的尾巴 → 它先取下尾巴,重新把 cells 转成内容块,再把尾巴接回末尾 → view.renderables 被更新。
调用关系:replace_cells、consolidate_cells、set_highlight_cell 调用它;它用 take_live_tail_renderable 和 render_cells 完成重建。
调用图:调用 1 个内部函数(take_live_tail_renderable);被 3 处调用(consolidate_cells, replace_cells, set_highlight_cell);外部调用 1 个(render_cells)。
TranscriptOverlay::take_live_tail_renderable702–704 ↗
fn take_live_tail_renderable(&mut self) -> Option<Box<dyn Renderable>>
作用:把末尾的实时尾巴内容块临时取下来。这样重建历史内容时不会把尾巴弄丢或重复。
数据流:进去的是当前 renderables 和 cells 数量 → 如果可绘制块比已提交消息多,就弹出最后一块 → 出来的是可选尾巴内容块,并改变 renderables。
调用关系:insert_cell、rebuild_renderables、sync_live_tail 都用它;它依赖一个约定:实时尾巴永远放在最后。
调用图:被 3 处调用(insert_cell, rebuild_renderables, sync_live_tail)。
TranscriptOverlay::live_tail_renderable706–722 ↗
fn live_tail_renderable(
lines: Vec<HyperlinkLine>,
has_prior_cells: bool,
is_stream_continuation: bool,
) -> Box<dyn Renderable>
作用:把实时尾巴文本行包装成可显示内容,并按需要加上和上一条消息之间的空行。
数据流:进去的是带链接文本行、是否已有历史消息、是否是上一段的延续 → 它创建 HyperlinkLinesRenderable 并缓存高度,如果不是连续流且前面有消息就加顶部空白 → 出来的是一个 Renderable。
调用关系:TranscriptOverlay::sync_live_tail 在需要刷新尾巴时调用它;它组合 CachedRenderable、HyperlinkLinesRenderable 和 InsetRenderable。
TranscriptOverlay::render_hints724–771 ↗
fn render_hints(&self, area: Rect, buf: &mut Buffer)
作用:绘制 transcript 页面底部两行快捷键提示。它会根据是否有高亮消息显示不同的编辑提示。
数据流:进去的是底部区域和缓冲区 → 它准备滚动、翻页、跳转、退出、编辑上一条/下一条等提示 → 调用 render_key_hints 写到缓冲区。
调用关系:TranscriptOverlay::render 在画完主体后调用它;它使用 first_or_empty 缩短按键显示。
调用图:调用 2 个内部函数(first_or_empty, render_key_hints);被 1 处调用(render);外部调用 2 个(new, vec!)。
TranscriptOverlay::render773–779 ↗
fn render(&mut self, area: Rect, buf: &mut Buffer)
作用:绘制完整 transcript 覆盖层。上面是可滚动记录,下面是快捷键提示。
数据流:进去的是全屏区域和缓冲区 → 它把区域分成上方正文和下方三行提示 → 先让 PagerView 画正文,再画提示。
调用关系:TranscriptOverlay::handle_event 在 Draw 或 Resize 时通过 tui.draw 调用它;测试里的 transcript_line_numbers 也直接用它检查可见内容。
调用图:调用 2 个内部函数(render, render_hints);被 1 处调用(transcript_line_numbers);外部调用 1 个(new)。
TranscriptOverlay::handle_event783–802 ↗
fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()>
作用:处理 transcript 覆盖层收到的事件。关闭键会结束页面,滚动键会交给 PagerView,重绘事件会画页面。
数据流:进去的是 TUI 和事件 → 如果是关闭键就设置 is_done;如果是其他键就交给 PagerView;如果是绘制或缩放就调用 tui.draw 渲染 → 返回处理结果。
调用关系:Overlay::handle_event 会把 Transcript 类型的事件交给它;它是 transcript 页面运行时的事件调度点。
调用图:调用 1 个内部函数(handle_key_event);外部调用 1 个(draw)。
TranscriptOverlay::is_done803–805 ↗
fn is_done(&self) -> bool
作用:返回 transcript 页面是否已经请求关闭。用户按退出键后这里会变成真。
数据流:进去的是当前对象状态 → 它读取 is_done 字段 → 出来的是布尔值。
调用关系:Overlay::is_done 会转问它,主循环据此关闭覆盖层。
TranscriptOverlay::committed_cell_count808–810 ↗
fn committed_cell_count(&self) -> usize
作用:在测试里查看 transcript 里有多少条已提交消息。它帮助确认插入、替换、合并是否正确。
数据流:进去的是 overlay 状态 → 它读取 cells 的长度 → 出来的是数量。
调用关系:这是只在测试配置下编译的辅助函数,不参与正常运行。
StaticOverlay::with_title819–830 ↗
fn with_title(
lines: Vec<Line<'static>>,
title: String,
keymap: PagerKeymap,
) -> Self
作用:用普通文本行创建一个带标题的静态覆盖层。它把文本包装成可换行段落。
数据流:进去的是文本行、标题和按键配置 → 它创建 Paragraph 并加换行设置,再交给 with_renderables → 出来的是 StaticOverlay。
调用关系:Overlay::new_static_with_lines 和测试辅助 static_overlay 调用它;真正通用构造交给 StaticOverlay::with_renderables。
调用图:被 2 处调用(new_static_with_lines, static_overlay);外部调用 4 个(new, with_renderables, from, vec!)。
StaticOverlay::with_renderables832–841 ↗
fn with_renderables(
renderables: Vec<Box<dyn Renderable>>,
title: String,
keymap: PagerKeymap,
) -> Self
作用:用一组可绘制内容创建静态覆盖层。适合内容不只是普通文本的情况。
数据流:进去的是可绘制块、标题和按键配置 → 它创建 PagerView,初始滚动在顶部,并设置未完成状态 → 出来的是 StaticOverlay。
调用关系:Overlay::new_static_with_renderables 和 with_title 都会走到这里;后续绘制、滚动由内部 PagerView 完成。
调用图:调用 1 个内部函数(new);被 1 处调用(new_static_with_renderables)。
StaticOverlay::render_hints843–876 ↗
fn render_hints(&self, area: Rect, buf: &mut Buffer)
作用:绘制静态页底部快捷键提示。它只显示滚动、翻页、跳转和退出这些阅读操作。
数据流:进去的是底部区域和缓冲区 → 它准备两行提示内容 → 调用 render_key_hints 写入缓冲区。
调用关系:StaticOverlay::render 在主体绘制后调用它;它复用 first_or_empty 和 render_key_hints。
调用图:调用 2 个内部函数(first_or_empty, render_key_hints);被 1 处调用(render);外部调用 2 个(new, vec!)。
StaticOverlay::render878–884 ↗
fn render(&mut self, area: Rect, buf: &mut Buffer)
作用:绘制完整静态覆盖层。结构和 transcript 类似:上方内容,下方提示。
数据流:进去的是屏幕区域和缓冲区 → 它切出上方主体和下方三行提示 → 先画 PagerView,再画快捷键提示。
调用关系:StaticOverlay::handle_event 在需要重绘时通过 tui.draw 调用它。
调用图:调用 2 个内部函数(render, render_hints);外部调用 1 个(new)。
StaticOverlay::handle_event888–905 ↗
fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()>
作用:处理静态覆盖层的事件。退出键关闭页面,滚动键交给通用 PagerView。
数据流:进去的是 TUI 和事件 → 关闭键会设置 is_done;其他键交给 PagerView;绘制或缩放会调用 render → 返回处理结果。
调用关系:Overlay::handle_event 会把 Static 类型的事件交给它;它和 TranscriptOverlay::handle_event 是并列的运行入口。
调用图:调用 1 个内部函数(handle_key_event);外部调用 1 个(draw)。
StaticOverlay::is_done906–908 ↗
fn is_done(&self) -> bool
作用:告诉外部静态页是否已经关闭。它用于主循环清理覆盖层。
数据流:进去的是当前状态 → 它读取 is_done → 出来是真或假。
调用关系:Overlay::is_done 会调用它,保持外部统一判断方式。
render_offset_content911–936 ↗
fn render_offset_content(
area: Rect,
buf: &mut Buffer,
renderable: &dyn Renderable,
scroll_offset: u16,
) -> u16
作用:绘制一个顶部已经被滚掉一部分的内容块。它像先把整块画到临时纸上,再剪下可见部分贴到屏幕上。
数据流:进去的是目标区域、目标缓冲区、内容块和内部滚动偏移 → 它创建临时缓冲区,先让内容块画完整可需部分,再把偏移后的可见行复制到目标缓冲区 → 返回复制了多少行。
调用关系:PagerView::render_content 在内容块半露半藏时调用它;这是正确裁剪复杂内容块的关键。
调用图:被 1 处调用(render_content);外部调用 4 个(empty, new, desired_height, render)。
tests::TestCell::display_lines966–968 ↗
fn display_lines(&self, _width: u16) -> Vec<Line<'static>>
作用:测试用的假历史消息返回要显示的行。它让测试不用依赖真实聊天消息类型。
数据流:进去的是宽度但测试里不使用 → 它复制保存的 lines → 出来的是显示行列表。
调用关系:测试构造 TranscriptOverlay 时通过 HistoryCell 接口间接调用它。
tests::TestCell::raw_lines970–972 ↗
fn raw_lines(&self) -> Vec<Line<'static>>
作用:测试用假消息返回原始文本行。它满足 HistoryCell 接口要求。
数据流:进去的是 TestCell → 它复制内部 lines → 出来的是原始行列表。
调用关系:只在测试环境中作为 HistoryCell 的一部分存在,支撑各种 transcript 渲染测试。
tests::TestCell::transcript_lines974–976 ↗
fn transcript_lines(&self, _width: u16) -> Vec<Line<'static>>
作用:测试用假消息返回 transcript 页面要用的文本行。这里直接返回保存的行。
数据流:进去的是宽度但不使用 → 它复制内部 lines → 出来的是 transcript 行列表。
调用关系:TranscriptOverlay 渲染测试通过 HistoryCell 接口使用它。
tests::paragraph_block979–986 ↗
tests::default_pager_keymap988–990 ↗
fn default_pager_keymap() -> crate::keymap::PagerKeymap
作用:拿一份默认翻页按键配置给测试用。这样测试不用手写每个快捷键。
数据流:进去没有参数 → 它读取 RuntimeKeymap 的默认值并取出 pager 部分 → 出来的是 PagerKeymap。
调用关系:测试辅助 transcript_overlay、static_overlay、pager_view 都调用它。
调用图:调用 1 个内部函数(defaults)。
tests::transcript_overlay992–994 ↗
fn transcript_overlay(cells: Vec<Arc<dyn HistoryCell>>) -> TranscriptOverlay
作用:测试里快速创建 TranscriptOverlay。它减少重复样板代码。
数据流:进去的是历史消息列表 → 它取默认按键配置并调用 TranscriptOverlay::new → 出来的是记录覆盖层。
调用关系:大多数 transcript 相关测试都通过它创建对象。
调用图:调用 1 个内部函数(new);外部调用 1 个(default_pager_keymap)。
tests::static_overlay996–998 ↗
fn static_overlay(lines: Vec<Line<'static>>, title: &str) -> StaticOverlay
作用:测试里快速创建 StaticOverlay。它把文本行和标题包装成静态页。
数据流:进去的是文本行和标题 → 它取默认按键配置并调用 StaticOverlay::with_title → 出来的是静态覆盖层。
调用关系:静态页快照和换行测试会用它。
调用图:调用 1 个内部函数(with_title);外部调用 1 个(default_pager_keymap)。
tests::pager_view1000–1011 ↗
fn pager_view(
renderables: Vec<Box<dyn Renderable>>,
title: &str,
scroll_offset: usize,
) -> PagerView
作用:测试里直接创建 PagerView。用于绕开外层 overlay,专门检查滚动和高度算法。
数据流:进去的是内容块、标题和初始滚动位置 → 它加上默认按键配置并调用 PagerView::new → 出来的是 PagerView。
调用关系:PagerView 的高度、定位、到底判断测试都用它。
调用图:调用 1 个内部函数(new);外部调用 1 个(default_pager_keymap)。
tests::edit_prev_hint_is_visible1014–1029 ↗
fn edit_prev_hint_is_visible()
作用:确认 transcript 底部能看到“编辑上一条”的提示。这个提示对用户找编辑入口很重要。
数据流:进去没有业务输入 → 它创建一条假消息的 overlay,渲染到宽缓冲区,转成文字 → 断言里面包含 edit prev。
调用关系:它测试 TranscriptOverlay::render_hints 的实际输出。
调用图:外部调用 6 个(empty, new, assert!, buffer_to_text, transcript_overlay, vec!)。
tests::edit_next_hint_is_visible_when_highlighted1032–1048 ↗
fn edit_next_hint_is_visible_when_highlighted()
作用:确认有高亮消息时会显示“编辑下一条”的提示。没有这个,编辑导航提示会不完整。
数据流:进去没有参数 → 它创建 overlay、设置高亮、渲染、转文本 → 断言包含 edit next。
调用关系:它覆盖 TranscriptOverlay::set_highlight_cell 对底部提示的影响。
调用图:外部调用 6 个(empty, new, assert!, buffer_to_text, transcript_overlay, vec!)。
tests::transcript_overlay_snapshot_basic1051–1068 ↗
fn transcript_overlay_snapshot_basic()
作用:用快照检查基础 transcript 页面长什么样。快照测试就是把输出画面保存下来,以后防止无意变化。
数据流:进去没有参数 → 它创建三条假消息,渲染到测试终端 → 和保存的快照比对。
调用关系:它验证 TranscriptOverlay::render 和 PagerView::render 的整体配合。
调用图:外部调用 5 个(new, assert_snapshot!, new, transcript_overlay, vec!)。
tests::transcript_overlay_preserves_semantic_web_links1071–1089 ↗
fn transcript_overlay_preserves_semantic_web_links()
作用:确认 transcript 里的网页链接不会在渲染后丢失。文字换行时链接标记很容易出错,所以要专门测。
数据流:进去没有参数 → 它创建含长链接的消息,渲染到窄区域 → 检查缓冲区里还能找到终端超链接标记。
调用关系:它主要验证 CellRenderable::render 和 mark_buffer_hyperlinks 的配合。
调用图:外部调用 5 个(empty, new, assert!, transcript_overlay, vec!)。
tests::transcript_overlay_renders_live_tail1092–1110 ↗
fn transcript_overlay_renders_live_tail()
作用:确认 transcript 能显示正在生成的实时尾巴。用户打开记录页时应该也能看到当前流式输出。
数据流:进去没有参数 → 它创建已有消息,调用 sync_live_tail 加入 tail,再渲染 → 用快照验证画面。
调用关系:它测试 TranscriptOverlay::sync_live_tail、live_tail_renderable 和 render 的组合。
调用图:外部调用 5 个(new, assert_snapshot!, new, transcript_overlay, vec!)。
tests::transcript_overlay_live_tail_preserves_semantic_web_links1113–1141 ↗
fn transcript_overlay_live_tail_preserves_semantic_web_links()
作用:确认实时尾巴里的链接也能保留下来。不是只有已提交消息才应该支持链接。
数据流:进去没有参数 → 它把一个含链接的活动消息转换成尾巴行,渲染 overlay → 检查缓冲区中存在链接标记。
调用关系:它验证 HyperlinkLinesRenderable::render 在 live tail 场景下的链接处理。
调用图:调用 1 个内部函数(new);外部调用 6 个(empty, new, new, assert!, new, transcript_overlay)。
tests::transcript_overlay_sync_live_tail_is_noop_for_identical_key1144–1166 ↗
fn transcript_overlay_sync_live_tail_is_noop_for_identical_key()
作用:确认实时尾巴版本没变时不会重复计算。这样可以避免无意义的昂贵刷新。
数据流:进去没有参数 → 它用同一个 key 调用 sync_live_tail 两次,并统计生成函数被调用次数 → 断言只调用一次。
调用关系:它直接测试 TranscriptOverlay::sync_live_tail 的缓存键逻辑。
调用图:外部调用 4 个(assert_eq!, new, transcript_overlay, vec!)。
tests::buffer_to_text1168–1186 ↗
fn buffer_to_text(buf: &Buffer, area: Rect) -> String
作用:把测试绘图缓冲区转成普通字符串,方便断言和快照。它把不可见的终端格子变成可读文本。
数据流:进去的是缓冲区和区域 → 它逐行逐列读取符号,空格补齐并裁掉行尾空白 → 出来的是多行字符串。
调用关系:多个测试用它检查渲染后的文字内容。
调用图:外部调用 3 个(bottom, right, new)。
tests::transcript_overlay_apply_patch_scroll_vt100_clears_previous_page1189–1251 ↗
fn transcript_overlay_apply_patch_scroll_vt100_clears_previous_page()
作用:确认 transcript 翻滚后旧页面内容会被清干净。终端复用缓冲区时,如果不清,旧字符可能残留。
数据流:进去没有参数 → 它构造包含补丁、审批、命令输出的复杂历史,先渲染到底部再滚到顶部重渲染 → 把结果转文本并做快照。
调用关系:它验证 PagerView::render_content 的清屏和空白填充行为,尤其是复杂消息场景。
调用图:外部调用 15 个(new, empty, from_millis, new, from, new, new, assert_snapshot!, new_active_exec_command, new_patch_event (+5 more))。
tests::transcript_overlay_keeps_scroll_pinned_at_bottom1254–1278 ↗
fn transcript_overlay_keeps_scroll_pinned_at_bottom()
作用:确认用户原本在底部时,新消息加入后仍然跟到底部。这是聊天记录常见的“跟随最新消息”行为。
数据流:进去没有参数 → 它创建很多行,渲染后确认在底部,插入新消息 → 断言滚动位置被设成底部标记。
调用关系:它测试 TranscriptOverlay::insert_cell 对 PagerView::is_scrolled_to_bottom 的使用。
调用图:外部调用 7 个(new, new, assert!, assert_eq!, new, transcript_overlay, vec!)。
tests::transcript_overlay_preserves_manual_scroll_position1281–1302 ↗
fn transcript_overlay_preserves_manual_scroll_position()
作用:确认用户手动滚到上面看旧内容时,新消息不会把他强行拉到底部。
数据流:进去没有参数 → 它创建长记录并渲染,再手动把 scroll_offset 设为顶部,插入新消息 → 断言位置仍在顶部。
调用关系:它和前一个测试成对验证 insert_cell 的滚动保护逻辑。
调用图:外部调用 6 个(new, new, assert_eq!, new, transcript_overlay, vec!)。
tests::transcript_overlay_consolidation_remaps_highlight_inside_range1305–1329 ↗
fn transcript_overlay_consolidation_remaps_highlight_inside_range()
作用:确认被合并范围内的高亮消息会指向合并后的新消息。否则编辑焦点可能丢失或指错。
数据流:进去没有参数 → 它创建多条消息,高亮第 3 条,合并 2 到 5 的范围 → 断言高亮变为合并后的位置。
调用关系:它测试 TranscriptOverlay::consolidate_cells 对范围内高亮的修正。
调用图:外部调用 4 个(new, assert_eq!, transcript_overlay, vec!)。
tests::transcript_overlay_consolidation_remaps_highlight_after_range1332–1356 ↗
fn transcript_overlay_consolidation_remaps_highlight_after_range()
作用:确认合并范围后面的高亮会向前平移。因为几条消息合成一条后,后面的序号会变小。
数据流:进去没有参数 → 它创建多条消息,高亮合并范围之后的消息,执行合并 → 断言高亮序号按删除数量修正。
调用关系:它测试 TranscriptOverlay::consolidate_cells 对范围后高亮的修正。
调用图:外部调用 4 个(new, assert_eq!, transcript_overlay, vec!)。
tests::static_overlay_snapshot_basic1359–1369 ↗
fn static_overlay_snapshot_basic()
作用:用快照检查静态覆盖层的基础画面。它防止标题、正文、提示栏的布局被意外改坏。
数据流:进去没有参数 → 它创建三行文本的静态页,渲染到测试终端 → 和快照比对。
调用关系:它验证 StaticOverlay::render 和 PagerView::render 的整体效果。
调用图:外部调用 5 个(new, assert_snapshot!, new, static_overlay, vec!)。
tests::transcript_line_numbers1372–1395 ↗
fn transcript_line_numbers(overlay: &mut TranscriptOverlay, area: Rect) -> Vec<usize>
作用:测试辅助函数,渲染 transcript 后提取当前可见的 line-NN 编号。它让分页测试能精确判断看到的是哪几行。
数据流:进去的是 overlay 和区域 → 它渲染到缓冲区,切出正文区域,扫描每行里的 line-数字 → 出来的是可见编号列表。
调用关系:transcript_overlay_paging_is_continuous_and_round_trips 用它检查翻页连续性。
tests::transcript_overlay_paging_is_continuous_and_round_trips1398–1463 ↗
fn transcript_overlay_paging_is_continuous_and_round_trips()
作用:确认 PageDown 和 PageUp 翻页连续,并且来回翻能回到原位置。这样用户不会漏看或重复太多内容。
数据流:进去没有参数 → 它创建 50 行记录,先渲染初始化布局,再模拟不同位置的翻页 → 用可见行号断言页面连续和往返一致。
调用关系:它重点验证 PagerView::page_height、render 和滚动偏移计算。
调用图:外部调用 5 个(empty, new, assert_eq!, transcript_line_numbers, transcript_overlay)。
tests::static_overlay_wraps_long_lines1466–1475 ↗
fn static_overlay_wraps_long_lines()
作用:确认静态页中的长行会在窄窗口里自动换行。否则内容会被截断看不全。
数据流:进去没有参数 → 它创建一条很长的静态文本,渲染到窄测试终端 → 用快照确认换行效果。
调用关系:它验证 StaticOverlay::with_title 创建的 Paragraph 换行设置。
调用图:外部调用 5 个(new, assert_snapshot!, new, static_overlay, vec!)。
tests::pager_view_content_height_counts_renderables1478–1489 ↗
fn pager_view_content_height_counts_renderables()
作用:确认 PagerView 会把多个内容块的高度正确相加。总高度算错会导致滚动范围错。
数据流:进去没有参数 → 它创建两个段落块,调用 content_height → 断言总高度等于两块行数之和。
调用关系:它直接测试 PagerView::content_height。
调用图:外部调用 3 个(assert_eq!, pager_view, vec!)。
tests::pager_view_ensure_chunk_visible_scrolls_down_when_needed1492–1524 ↗
fn pager_view_ensure_chunk_visible_scrolls_down_when_needed()
作用:确认目标内容块在下面看不见时,PagerView 会向下滚到它。用于高亮消息自动定位。
数据流:进去没有参数 → 它创建三块内容,把目标设为第三块,调用 ensure_chunk_visible 后渲染 → 检查第三块几行都出现在画面中。
调用关系:它测试 PagerView::ensure_chunk_visible 的向下滚动行为。
调用图:外部调用 6 个(empty, new, assert!, buffer_to_text, pager_view, vec!)。
tests::pager_view_ensure_chunk_visible_scrolls_up_when_needed1527–1543 ↗
fn pager_view_ensure_chunk_visible_scrolls_up_when_needed()
作用:确认目标内容块在上面看不见时,PagerView 会向上滚。这样自动定位不只会往下走。
数据流:进去没有参数 → 它创建内容块,把滚动位置放到较下面,再要求显示第一块 → 断言 scroll_offset 回到 0。
调用关系:它测试 PagerView::ensure_chunk_visible 的向上滚动行为。
调用图:外部调用 4 个(new, assert_eq!, pager_view, vec!)。
tests::pager_view_is_scrolled_to_bottom_accounts_for_wrapped_height1546–1569 ↗
fn pager_view_is_scrolled_to_bottom_accounts_for_wrapped_height()
作用:确认“是否到底部”的判断会考虑换行后的真实高度。宽度变窄导致行数变多时尤其重要。
数据流:进去没有参数 → 它创建高内容块并渲染,先断言顶部不是底部,再把滚动设到底部并重渲染 → 断言此时是底部。
调用关系:它验证 PagerView::is_scrolled_to_bottom 和 render 记录高度的配合。
调用图:外部调用 5 个(empty, new, assert!, pager_view, vec!)。
键位映射编辑流程
这些文件定义可配置的键位映射操作,并构建选择器、编辑器、调试检查器,以及用于交互式重映射的聊天控件集成。
tui/src/chatwidget/keymap_picker.rs源码 ↗
这个文件解决的是一个很实际的问题:用户改了快捷键以后,界面上看到的设置、程序记住的配置、真正响应键盘的那套规则,必须一起变。否则就会出现“菜单显示已经改了,但按键还是旧的”这种很迷惑的情况。这里的代码不负责定义快捷键本身,也不负责画复杂菜单;这些交给 keymap_setup 和 RuntimeKeymap。它更像一个前台接待员:打开快捷键总菜单,打开某个动作的子菜单,打开“请按一个键”的捕获界面,或者打开调试查看器。等一次修改真正生效后,它会同时更新内存里的配置、复制上一条回复的快捷键缓存、聊天区快捷键、底部面板快捷键,并要求界面重画。一个重要点是,它会先验证当前配置能不能解析成可运行的快捷键表;如果配置坏了,就直接报错,不让用户在半坏的状态里继续编辑。
ChatWidget::open_keymap_picker30–44 ↗
fn open_keymap_picker(&mut self)
作用:打开 /keymap 的主菜单,让用户看到当前有哪些动作可以改快捷键。它会先检查现有快捷键配置是否有效,避免用一份坏配置生成菜单。
数据流:进去的是当前聊天窗口对象,它里面带着保存下来的 tui_keymap 配置。函数先把这份配置交给 RuntimeKeymap::from_config,尝试变成程序实际能用的快捷键表;成功后,再结合 keymap_action_filter 给出的筛选条件,生成主菜单参数,并显示到底部面板。失败时,它不会显示菜单,而是在聊天界面里加一条错误消息,告诉用户配置无效。
调用关系:这是用户进入快捷键设置流程的起点之一。它自己不制作菜单内容,而是把配置验证交给 from_config,把菜单参数生成交给 build_keymap_picker_params_with_filter;它还会调用 ChatWidget::keymap_action_filter,决定哪些动作应该出现在菜单里。
调用图:调用 2 个内部函数(keymap_action_filter, from_config);外部调用 2 个(format!, build_keymap_picker_params_with_filter)。
ChatWidget::open_keymap_capture71–87 ↗
fn open_keymap_capture(
&mut self,
context: String,
action: String,
intent: KeymapEditIntent,
runtime_keymap: &RuntimeKeymap,
)
作用:打开“请按下新的快捷键”的捕获界面,用来设置、替换或添加一个按键绑定。简单说,就是让程序进入等待用户按键的状态。
数据流:进去的是场景、动作名、编辑意图 KeymapEditIntent,以及当前运行时快捷键表。函数把这些信息和聊天窗口的事件发送器一起交给 build_keymap_capture_view,生成一个捕获按键的视图;然后把这个视图装进底部面板,并请求界面重画。之后用户按下的键会通过事件通道回到正常的应用事件流程里。
调用关系:它通常由某个动作菜单触发,比如用户选择“设置新快捷键”之后。它把真正识别按键的工作交给 build_keymap_capture_view 创建的视图;这里特别保留通过应用事件通道返回结果,是为了后续能走统一的保存配置和刷新快捷键流程。
调用图:调用 1 个内部函数(build_keymap_capture_view);外部调用 1 个(new)。
ChatWidget::open_keymap_debug90–94 ↗
fn open_keymap_debug(&mut self, runtime_keymap: &RuntimeKeymap)
作用:打开快捷键调试查看器,让用户或开发者查看当前按键会被解释成什么。它适合排查“我按了这个键,为什么没反应或反应不对”。
数据流:进去的是当前运行时快捷键表。函数把它和保存的配置交给 build_keymap_debug_view,生成一个调试视图;再把视图放到底部面板里,并请求界面重画。它不会修改配置,只是展示当前状态。
调用关系:这是快捷键设置流程里的辅助入口。它依赖 build_keymap_debug_view 来生成具体查看界面,自己负责把这个界面接入 ChatWidget 的底部面板显示系统。
调用图:外部调用 2 个(new, build_keymap_debug_view)。
ChatWidget::return_to_keymap_picker118–150 ↗
fn return_to_keymap_picker(
&mut self,
context: &str,
action: &str,
runtime_keymap: &RuntimeKeymap,
)
作用:一次快捷键编辑完成后,回到主快捷键菜单,并且把刚才编辑的那个动作高亮选中。这样用户能清楚看到自己刚改的是哪一项,也方便继续改。
数据流:进去的是刚编辑过的场景、动作名和当前运行时快捷键表。函数先用这些信息、当前配置和 keymap_action_filter 生成一个“主菜单并选中指定动作”的菜单参数。然后它尝试把当前底部面板里旧的快捷键相关页面直接替换成这个新主菜单;如果发现当前页面栈已经不是预期的快捷键页面,就退一步显示一个全新的主菜单。最后请求界面重画。
调用关系:它通常在保存或应用一次快捷键修改之后被调用。它会调用 ChatWidget::keymap_action_filter 来保持菜单筛选规则一致,并把菜单参数生成交给 build_keymap_picker_params_for_selected_action_with_filter。它还负责避免底部面板里堆积过时的子菜单。
调用图:调用 1 个内部函数(keymap_action_filter);外部调用 1 个(build_keymap_picker_params_for_selected_action_with_filter)。
ChatWidget::keymap_action_filter152–156 ↗
fn keymap_action_filter(&self) -> keymap_setup::KeymapActionFilter
作用:生成快捷键菜单的筛选条件,决定哪些动作应该显示。当前主要看一个因素:快速模式是否开启。
数据流:进去的是聊天窗口当前状态。函数读取 fast_mode_enabled() 的结果,把它包装成 KeymapActionFilter 返回。它不改动界面,也不改配置,只产出一个给菜单构建器使用的小条件对象。
调用关系:它是主菜单构建时的辅助函数,被 ChatWidget::open_keymap_picker 和 ChatWidget::return_to_keymap_picker 调用。这样打开菜单和编辑后返回菜单时,使用的是同一套筛选规则,不会前后显示不一致。
调用图:被 2 处调用(open_keymap_picker, return_to_keymap_picker)。
ChatWidget::apply_keymap_update164–180 ↗
fn apply_keymap_update(
&mut self,
keymap_config: TuiKeymap,
runtime_keymap: &RuntimeKeymap,
)
作用:把一次已经确认并保存的快捷键修改,真正应用到正在运行的聊天界面里。它的重点是“一起更新”,避免配置、快捷键响应和界面提示互相打架。
数据流:进去的是新的 TuiKeymap 配置和对应的运行时快捷键表。函数先替换聊天窗口内存里的配置;再更新应用级的“复制上一条回复”快捷键缓存、聊天区快捷键表;接着根据终端信息重新计算“编辑排队消息”的提示按键,并同步给底部面板;然后把整套运行时快捷键绑定也交给底部面板。最后请求界面重画。出来的结果不是返回值,而是整个正在运行的界面状态被同步到了新快捷键。
调用关系:它通常在外层代码已经把配置写入文件之后调用。它会用 terminal_info 获取当前终端能力,用 queued_message_edit_hint_binding 算出合适的提示按键。它是快捷键编辑流程的收尾步骤,确保用户改完后不用重启,界面和按键处理马上按新规则工作。
调用图:外部调用 2 个(terminal_info, queued_message_edit_hint_binding)。
tui/src/keymap_setup.rs源码 ↗
这个文件解决的是“用户想在终端界面里改快捷键,但不能把配置改乱”的问题。它像一个三步办事窗口:先列出可改的动作,再根据当前快捷键显示“替换、加一个备用键、删掉自定义设置”等选项,最后临时打开一个捕获窗口,只收一次按键。它不会自己写配置文件,而是发出应用事件,让更外层决定保存和报错。重要的是,它用运行时快捷键表来判断当前真实生效的按键,所以默认配置、全局兜底和用户自定义都能正确显示;写入时又只写用户选中的 tui.keymap.<context>.<action> 位置。这样可以避免界面看到的是一套快捷键、实际保存却改到另一套的混乱。
key_binding_span85–91 ↗
fn key_binding_span(binding: &str) -> ratatui::text::Span<'static>
作用:把一个快捷键文字变成界面上带颜色的片段。没有绑定时用暗色,普通快捷键用青色,方便用户一眼分辨。
数据流:输入一个快捷键字符串 → 判断它是不是 unbound → 输出一个可渲染的文字片段,不改动任何配置。
调用关系:它是界面显示的小工具,主要被菜单头部用来把当前快捷键显示得更清楚。
open_capture_action102–114 ↗
fn open_capture_action(
context: String,
action: String,
intent: KeymapEditIntent,
) -> Box<dyn Fn(&AppEventSender) + Send + Sync>
作用:做一个“之后打开按键捕获窗口”的动作。用户选中某个菜单项时,这个动作会发事件给应用。
数据流:输入上下文、动作名和编辑意图 → 包成一个闭包 → 闭包执行时发送 OpenKeymapCapture 事件。
调用关系:它不直接打开界面,而是把请求交给应用事件系统;action_menu_item 会用它给菜单项挂上后续动作。
调用图:外部调用 1 个(new)。
build_keymap_conflict_params356–395 ↗
fn build_keymap_conflict_params(
context: String,
action: String,
key: String,
intent: KeymapEditIntent,
error: String,
) -> SelectionViewParams
作用:生成快捷键冲突时的提示弹窗。它告诉用户这个键不能用,并给出“重新选一个键”或“取消”。
数据流:输入目标动作、新键、编辑意图和错误文字 → 包装成弹窗菜单 → 输出 SelectionViewParams。
调用关系:apply_keymap_capture 在发现新按键不合法或冲突后会调用它;如果用户选择重试,它再发 OpenKeymapCapture 回到捕获流程。
调用图:调用 1 个内部函数(standard_popup_hint_line);被 1 处调用(apply_keymap_capture);外部调用 4 个(default, from, format!, vec!)。
build_keymap_capture_view403–422 ↗
fn build_keymap_capture_view(
context: String,
action: String,
intent: KeymapEditIntent,
runtime_keymap: &RuntimeKeymap,
app_event_tx: AppEventSender,
) -> KeymapCaptureView
作用:创建一个临时的按键捕获界面,让用户按下一个新快捷键。
数据流:输入目标动作、编辑意图、当前运行时快捷键表和事件发送器 → 读取当前绑定并整理成显示文字 → 返回 KeymapCaptureView。
调用关系:open_keymap_capture 会调用它;它把显示和收键交给 KeymapCaptureView,真正保存仍由捕获后的应用事件处理。
调用图:调用 4 个内部函数(new, action_label, bindings_for_action, format_binding_summary);被 2 处调用(open_keymap_capture, capture_completion_returns_to_selected_keymap_picker_row)。
keymap_with_replacement425–432 ↗
fn keymap_with_replacement(
keymap: &TuiKeymap,
context: &str,
action: &str,
key: &str,
) -> Result<TuiKeymap, String>
作用:测试用的简化函数,把某个动作的快捷键直接替换成一个键。
数据流:输入原配置、动作位置和新键 → 调用 keymap_with_bindings 写成单个绑定 → 返回新配置或错误。
调用关系:它主要被测试用来快速准备“已经自定义过快捷键”的场景,底层实际改配置的活交给 keymap_with_bindings。
调用图:调用 1 个内部函数(keymap_with_bindings);被 9 处调用(action_menu_content_snapshot, capture_completion_returns_to_selected_keymap_picker_row, clear_completion_returns_to_selected_keymap_picker_row, clear_removes_custom_binding, debug_view_uses_custom_binding_source, picker_custom_render_snapshot, picker_customized_tab_contains_root_overrides, replacement_rejects_unknown_action, replacement_sets_single_binding)。
keymap_with_edit442–507 ↗
fn keymap_with_edit(
keymap: &TuiKeymap,
runtime_keymap: &RuntimeKeymap,
context: &str,
action: &str,
key: &str,
intent: &KeymapEditIntent,
) -> Result<KeymapEditOutcome, Strin
作用:把用户刚捕获到的按键应用到配置里。它支持替换全部、添加备用键、只替换其中一个,并会避免重复或过期选择。
数据流:输入旧配置、当前运行时快捷键表、目标动作、新键和编辑意图 → 先取当前真实生效绑定 → 算出下一组绑定 → 如无变化返回 Unchanged,如有变化返回带新配置和提示文字的 Updated。
调用关系:apply_keymap_capture 在收到 KeymapCaptured 后会调用它;它依赖 active_binding_specs 读取现状,必要时用 dedup_bindings 去重,最后交给 keymap_with_bindings 写入配置。
调用图:调用 3 个内部函数(active_binding_specs, dedup_bindings, keymap_with_bindings);被 8 处调用(apply_keymap_capture, add_alternate_duplicate_is_noop, add_alternate_grows_default_multi_binding, add_alternate_grows_single_binding, replace_all_collapses_multi_binding_to_single, replace_one_deduplicates_replacement, replace_one_preserves_other_bindings, replace_one_rejects_stale_old_key);外部调用 3 个(new, format!, vec!)。
keymap_with_bindings509–528 ↗
fn keymap_with_bindings(
keymap: &TuiKeymap,
context: &str,
action: &str,
keys: &[String],
) -> Result<TuiKeymap, String>
作用:把某个动作的快捷键列表写进一份新的根配置里。它是实际落到 tui.keymap 槽位的底层函数。
数据流:输入旧配置、动作位置和键列表 → 克隆配置 → 找到对应配置槽位 → 写成单个绑定或多个绑定 → 返回新配置。
调用关系:keymap_with_edit 和测试辅助 keymap_with_replacement 都靠它真正改配置;如果动作名不存在,它返回明确错误,提醒用户重新打开 /keymap。
调用图:调用 1 个内部函数(binding_slot);被 6 处调用(keymap_with_edit, keymap_with_replacement, action_menu_content_snapshot, replace_all_collapses_multi_binding_to_single, replace_one_deduplicates_replacement, replace_one_preserves_other_bindings);外部调用 4 个(new, Many, One, clone)。
active_binding_specs536–548 ↗
fn active_binding_specs(
runtime_keymap: &RuntimeKeymap,
context: &str,
action: &str,
) -> Result<Vec<String>, String>
作用:把运行时已经解析好的快捷键,转回配置文件里使用的字符串形式。这样显示和再次写入时用的是同一套写法。
数据流:输入运行时快捷键表和动作位置 → 找到该动作当前绑定 → 每个绑定转成配置字符串 → 返回字符串列表或未知动作错误。
调用关系:菜单构建和 keymap_with_edit 都用它获取“现在真正生效的键”;它把查找交给 bindings_for_action,把格式转换交给 binding_to_config_key_spec。
调用图:调用 1 个内部函数(bindings_for_action);被 3 处调用(build_keymap_action_menu_params, build_keymap_replace_binding_menu_params, keymap_with_edit)。
dedup_bindings550–557 ↗
fn dedup_bindings(bindings: Vec<String>) -> Vec<String>
作用:去掉快捷键列表里的重复项,同时保留第一次出现的顺序。
数据流:输入一个字符串列表 → 从前到后检查,没见过才放进新列表 → 返回去重后的列表。
调用关系:keymap_with_edit 在“替换其中一个绑定”后调用它,避免把两个绑定变成同一个键后留下重复配置。
调用图:被 1 处调用(keymap_with_edit);外部调用 1 个(new)。
keymap_without_custom_binding564–575 ↗
fn keymap_without_custom_binding(
keymap: &TuiKeymap,
context: &str,
action: &str,
) -> Result<TuiKeymap, String>
作用:删除某个动作在根配置里的自定义快捷键,让它重新使用默认或全局兜底规则。
数据流:输入旧配置和动作位置 → 克隆配置 → 找到对应槽位 → 把槽位设为 None → 返回新配置。
调用关系:apply_keymap_clear 会在用户选择“Remove custom binding”后调用它;它和设置空列表不同,None 的意思是恢复默认,不是强制解绑。
调用图:调用 1 个内部函数(binding_slot);被 2 处调用(apply_keymap_clear, clear_removes_custom_binding);外部调用 1 个(clone)。
has_custom_binding577–583 ↗
fn has_custom_binding(keymap: &TuiKeymap, context: &str, action: &str) -> Result<bool, String>
作用:检查某个动作在根配置里有没有用户自定义快捷键。
数据流:输入配置和动作位置 → 克隆配置并找到对应槽位 → 返回槽位是否有值。
调用关系:build_keymap_action_menu_params 用它决定“Remove custom binding”能不能点,以及菜单里显示来源是自定义还是默认。
调用图:调用 1 个内部函数(binding_slot);被 1 处调用(build_keymap_action_menu_params);外部调用 1 个(clone)。
KeymapCaptureView::new603–621 ↗
fn new(
context: String,
action: String,
intent: KeymapEditIntent,
label: String,
current_binding: String,
app_event_tx: AppEventSender,
) -> Self
作用:创建按键捕获视图的初始状态。它保存目标动作、当前绑定、事件发送器和是否完成等信息。
数据流:输入动作位置、编辑意图、显示标签、当前绑定文字和事件发送器 → 填入结构体字段 → 返回一个尚未完成、没有错误的捕获视图。
调用关系:build_keymap_capture_view 用它正式创建视图;测试也直接调用它来检查渲染效果。
调用图:被 2 处调用(build_keymap_capture_view, capture_view_snapshot)。
KeymapCaptureView::lines623–650 ↗
KeymapCaptureView::render654–656 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把按键捕获视图画到终端缓冲区里。
数据流:输入绘制区域和缓冲区 → 调用 lines 得到文字 → 用段落组件写入缓冲区。
调用关系:底部面板在需要显示这个临时视图时会调用它;测试里的 render_capture 也用它生成快照。
调用图:调用 1 个内部函数(lines);被 1 处调用(render_capture);外部调用 1 个(new)。
KeymapCaptureView::desired_height658–660 ↗
fn desired_height(&self, width: u16) -> u16
作用:告诉底部面板这个捕获视图需要多高。
数据流:输入可用宽度 → 调用 lines 计算会有多少行 → 返回行数作为高度。
调用关系:渲染布局前会用它估算空间;它和 render 共用 lines,避免高度和内容对不上。
调用图:调用 1 个内部函数(lines)。
KeymapCaptureView::handle_key_event664–688 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:处理用户在捕获视图里按下的键。Esc 取消,其他支持的键会被转换成配置字符串并发送出去。
数据流:输入一个终端按键事件 → 忽略按键释放事件;Esc 标记完成;其他键尝试转成配置写法 → 成功就发送 KeymapCaptured 并完成,失败就保存错误信息供界面显示。
调用关系:底部面板把按键事件交给它;它不直接改配置,而是通过 AppEventSender 把结果交给应用层继续处理。
调用图:调用 2 个内部函数(send, key_event_to_config_key_spec);外部调用 1 个(clone)。
KeymapCaptureView::is_complete690–692 ↗
fn is_complete(&self) -> bool
作用:告诉底部面板这个临时捕获视图是否该关闭。
数据流:读取 complete 字段 → 返回 true 或 false → 不改动状态。
调用关系:底部面板在处理完事件后会检查它;handle_key_event 和 on_ctrl_c 会把 complete 设为 true。
KeymapCaptureView::on_ctrl_c694–697 ↗
fn on_ctrl_c(&mut self) -> CancellationEvent
作用:处理 Ctrl+C 取消捕获。它把视图标记为完成,并说明这个取消已经被处理了。
数据流:没有额外输入 → 设置 complete 为 true → 返回 Handled。
调用关系:底部面板收到 Ctrl+C 时调用它;这样不会继续把 Ctrl+C 当作要绑定的新快捷键。
KeymapCaptureView::prefer_esc_to_handle_key_event699–701 ↗
fn prefer_esc_to_handle_key_event(&self) -> bool
作用:声明 Esc 应该先交给这个视图处理。因为在这里 Esc 的含义是取消捕获。
数据流:没有输入 → 固定返回 true → 不改状态。
调用关系:底部面板用它决定 Esc 的分发顺序,保证捕获视图能自己关闭。
key_event_to_config_key_spec704–706 ↗
fn key_event_to_config_key_spec(key_event: KeyEvent) -> Result<String, String>
作用:把终端按键事件转换成配置文件里能写的快捷键字符串。
数据流:输入 KeyEvent → 先转成项目内部的 KeyBinding → 再转成配置字符串或错误。
调用关系:KeymapCaptureView::handle_key_event 用它处理用户按下的新键;真正的格式细节继续交给 binding_to_config_key_spec。
调用图:调用 2 个内部函数(from_event, binding_to_config_key_spec);被 1 处调用(handle_key_event)。
binding_to_config_key_spec708–711 ↗
fn binding_to_config_key_spec(binding: KeyBinding) -> Result<String, String>
作用:把内部快捷键对象转成配置字符串。
数据流:输入 KeyBinding → 拆出按键本体和修饰键,比如 Ctrl、Alt、Shift → 调用 key_parts_to_config_key_spec 得到字符串。
调用关系:active_binding_specs 和 key_event_to_config_key_spec 都会走到它,保证显示已有绑定和保存新绑定使用同一套规则。
调用图:调用 2 个内部函数(parts, key_parts_to_config_key_spec);被 1 处调用(key_event_to_config_key_spec)。
key_parts_to_config_key_spec713–767 ↗
fn key_parts_to_config_key_spec(
code: KeyCode,
mut modifiers: KeyModifiers,
) -> Result<String, String>
作用:把具体按键和修饰键规范化成 ctrl-alt-x 这类配置写法,并拒绝不支持的键。
数据流:输入按键代码和修饰键 → 先规范化大小写和控制字符 → 检查只允许 Ctrl、Alt、Shift → 识别特殊键、功能键、空格、减号和可打印 ASCII 字符 → 返回标准字符串或错误原因。
调用关系:binding_to_config_key_spec 把所有转换请求交给它;它最后用 format_key_spec 统一拼接顺序。
调用图:调用 2 个内部函数(normalize_key_parts, format_key_spec);被 1 处调用(binding_to_config_key_spec);外部调用 3 个(difference, insert, format!)。
format_key_spec769–782 ↗
fn format_key_spec(modifiers: KeyModifiers, key: &str) -> String
作用:按固定顺序拼出快捷键字符串,比如先 ctrl、再 alt、再 shift,最后是键名。
数据流:输入修饰键集合和键名 → 依次检查是否包含 Ctrl、Alt、Shift → 拼成用短横线连接的字符串。
调用关系:key_parts_to_config_key_spec 在识别出键名后调用它,确保配置里的快捷键写法稳定一致。
调用图:被 1 处调用(key_parts_to_config_key_spec);外部调用 2 个(contains, new)。
tests::app_event_sender803–806 ↗
fn app_event_sender() -> AppEventSender
作用:测试里创建一个假的应用事件发送器。
数据流:创建一个无界消息通道 → 用发送端包装成 AppEventSender → 返回给测试使用。
调用关系:多个渲染和面板测试用它提供必要依赖,但通常不关心真实接收方。
调用图:调用 1 个内部函数(new);外部调用 1 个(unbounded_channel)。
tests::render_capture808–813 ↗
tests::render_debug815–821 ↗
fn render_debug(view: &KeymapDebugView, width: u16) -> String
作用:测试里把调试视图渲染成普通字符串。
数据流:输入调试视图和宽度 → 先问需要多高 → 渲染到缓冲区 → 用 render_buffer 转成文本。
调用关系:多个 debug_view 测试用它检查按键诊断界面的显示。
调用图:调用 2 个内部函数(desired_height, render);外部调用 3 个(empty, new, render_buffer)。
tests::render_picker823–827 ↗
fn render_picker(params: SelectionViewParams, width: u16) -> String
作用:测试里根据选择菜单参数渲染快捷键列表。
数据流:输入 SelectionViewParams 和宽度 → 创建 ListSelectionView → 调用 render_picker_from_view → 返回文本画面。
调用关系:快捷键选择器的快照测试用它验证宽屏、窄屏和自定义配置下的显示。
调用图:调用 2 个内部函数(new, defaults);外部调用 2 个(app_event_sender, render_picker_from_view)。
tests::render_picker_from_view829–835 ↗
fn render_picker_from_view(view: &ListSelectionView, width: u16) -> String
作用:测试里把已经创建好的选择列表视图渲染成字符串。
数据流:输入列表视图和宽度 → 计算高度 → 渲染到缓冲区 → 转成文本。
调用关系:tests::render_picker 把实际渲染工作交给它,也可用于已有视图的检查。
调用图:调用 2 个内部函数(desired_height, render);外部调用 3 个(empty, new, render_buffer)。
tests::fast_mode_action_filter837–841 ↗
fn fast_mode_action_filter() -> KeymapActionFilter
作用:测试里生成一个开启 Fast Mode 的动作过滤器。
数据流:没有输入 → 构造 fast_mode_enabled 为 true 的 KeymapActionFilter → 返回它。
调用关系:涉及 Fast Mode 是否出现在快捷键列表的测试会用它。
tests::render_buffer843–860 ↗
fn render_buffer(buf: &Buffer) -> String
作用:把 ratatui 的屏幕缓冲区转成可比较的普通文本。
数据流:输入缓冲区 → 按行按列读取每个格子的字符 → 去掉每行末尾空白 → 返回多行字符串。
调用关系:render_debug 和 render_picker_from_view 用它把终端画面变成快照测试能比对的内容。
调用图:外部调用 1 个(area)。
tests::test_pane862–876 ↗
fn test_pane() -> (BottomPane, AppEventSender, UnboundedReceiver<AppEvent>)
作用:测试里创建一个可用的底部面板、事件发送器和事件接收器。
数据流:创建消息通道和 AppEventSender → 用测试参数构造 BottomPane → 返回面板、发送器和接收器。
调用关系:捕获完成、清除完成和替换子菜单关闭等流程测试用它模拟真实底部面板。
调用图:调用 3 个内部函数(new, new, test_dummy);外部调用 1 个(new)。
tests::selection_tab878–884 ↗
fn selection_tab(params: &'a SelectionViewParams, id: &str) -> &'a SelectionTab
作用:从菜单参数里按 id 找到某个标签页,找不到就让测试失败。
数据流:输入菜单参数和标签页 id → 遍历 tabs → 返回匹配标签页引用。
调用关系:大量选择器测试用它读取 All、Common、Custom 等标签页。
tests::selection_item886–892 ↗
fn selection_item(params: &'a SelectionViewParams, name: &str) -> &'a SelectionItem
作用:从菜单参数里按名字找到某个菜单项,找不到就让测试失败。
数据流:输入菜单参数和菜单项名字 → 遍历 items → 返回匹配项引用。
调用关系:动作菜单测试用它检查某些按钮是否可用、是否会关闭菜单。
tests::picker_covers_every_replaceable_action911–937 ↗
fn picker_covers_every_replaceable_action()
作用:确认快捷键选择器覆盖了所有可替换动作,并且每个动作都有配置槽位和运行时绑定入口。
数据流:创建默认运行时表和开启 Fast Mode 的选择器参数 → 检查 All 标签页数量、菜单行为、配置槽位和运行时动作 → 测试通过或失败。
调用关系:它保护动作清单、配置结构和运行时快捷键表三者保持同步。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params_with_filter);外部调用 5 个(assert!, assert_eq!, default, fast_mode_action_filter, selection_tab)。
tests::picker_hides_fast_mode_action_when_feature_is_disabled940–951 ↗
fn picker_hides_fast_mode_action_when_feature_is_disabled()
作用:确认 Fast Mode 功能没开启时,快捷键列表里不会出现 Toggle Fast Mode。
数据流:用默认过滤条件生成选择器 → 查看 All 标签页 → 断言没有对应菜单项。
调用关系:它验证 picker 的功能开关逻辑,防止用户看到不能用的快捷键入口。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert!, default, selection_tab)。
tests::picker_shows_fast_mode_action_when_feature_is_enabled954–973 ↗
fn picker_shows_fast_mode_action_when_feature_is_enabled()
作用:确认 Fast Mode 开启时,相关动作会出现在多个合适的标签页里。
数据流:用 fast_mode_action_filter 生成选择器 → 读取 All、Common、App、Unbound 标签页 → 断言都有 Toggle Fast Mode。
调用关系:它和隐藏测试成对出现,保证过滤器开关两边都正确。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params_with_filter);外部调用 4 个(assert!, default, fast_mode_action_filter, selection_tab)。
tests::keymap_picker_fast_mode_enabled_snapshot976–988 ↗
fn keymap_picker_fast_mode_enabled_snapshot()
作用:保存 Fast Mode 开启时快捷键选择器的整体画面快照。
数据流:生成开启 Fast Mode 的选择器参数 → 渲染宽屏画面 → 和快照比对。
调用关系:它用于发现界面文案、排序或布局的意外变化。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params_with_filter);外部调用 3 个(assert_snapshot!, default, fast_mode_action_filter)。
tests::picker_common_tab_lists_curated_actions991–1034 ↗
fn picker_common_tab_lists_curated_actions()
作用:确认 Common 标签页列出的是人工挑选的常用动作,而且顺序正确。
数据流:生成默认选择器 → 取 Common 标签页 → 提取每项的 context.action → 和预期列表比较。
调用关系:它保护常用快捷键列表的内容,不让改动无意中影响新手最常看的页面。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert_eq!, default, selection_tab)。
tests::picker_approval_tab_lists_all_approval_actions1037–1068 ↗
fn picker_approval_tab_lists_all_approval_actions()
作用:确认 Approval 标签页包含所有审批相关动作。
数据流:生成默认选择器 → 取 approval-shortcuts 标签页 → 提取动作名 → 和预期审批动作列表比较。
调用关系:它保证审批界面的快捷键不会在 /keymap 里漏掉。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert_eq!, default, selection_tab)。
tests::picker_content_snapshot1071–1094 ↗
fn picker_content_snapshot()
作用:记录快捷键选择器的标签页数量和前几项内容,作为轻量快照。
数据流:生成默认选择器 → 统计每个标签页可选项 → 取 All 标签页前几项 → 和快照比对。
调用关系:它帮助发现排序、文案和标签页结构的变化。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert_snapshot!, default, selection_tab)。
tests::picker_customized_tab_contains_root_overrides1097–1120 ↗
fn picker_customized_tab_contains_root_overrides()
作用:确认用户自定义过的根配置会出现在 Custom 标签页,并在对应分类里显示自定义键。
数据流:先把 submit 改成 ctrl-enter → 生成运行时表和选择器 → 检查 Custom 与 Composer 标签页内容。
调用关系:它验证 keymap_with_replacement、RuntimeKeymap::from_config 和 picker 展示自定义来源的配合。
调用图:调用 3 个内部函数(from_config, keymap_with_replacement, build_keymap_picker_params);外部调用 4 个(assert!, assert_eq!, default, selection_tab)。
tests::picker_unbound_tab_lists_default_unbound_actions1123–1135 ↗
fn picker_unbound_tab_lists_default_unbound_actions()
作用:确认默认没有绑定的动作会出现在 Unbound 标签页。
数据流:生成默认选择器 → 读取 Unbound 标签页 → 检查数量、名字、显示为 unbound 且可选择。
调用关系:它保证未绑定动作不会被隐藏,用户仍然可以给它们设置快捷键。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 4 个(assert!, assert_eq!, default, selection_tab)。
tests::picker_debug_tab_is_last_and_opens_inspector1138–1157 ↗
fn picker_debug_tab_is_last_and_opens_inspector()
作用:确认 Debug 标签页在最后,并提供按键检查器入口。
数据流:生成默认选择器 → 查看最后一个标签页 → 检查 id、标题、说明和底部提示。
调用关系:它保护 /keymap 里的诊断入口,方便用户排查终端到底发来了什么键。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert!, assert_eq!, default)。
tests::picker_selected_action_starts_on_matching_all_tab_row1160–1175 ↗
fn picker_selected_action_starts_on_matching_all_tab_row()
作用:确认刷新选择器后会自动选中刚编辑过的动作。
数据流:为 composer.submit 生成带初始选择的参数 → 查看初始标签页和选中下标 → 和 All 标签页里的 Submit 行比较。
调用关系:编辑成功后返回列表会用同类参数;这个测试保证用户不会迷路。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params_for_selected_action);外部调用 3 个(assert_eq!, default, selection_tab)。
tests::picker_all_tab_items_remain_searchable1178–1198 ↗
fn picker_all_tab_items_remain_searchable()
作用:确认 All 标签页的菜单项仍带有搜索用文字。
数据流:生成默认选择器 → 取 All 标签页前几项的名字、说明、搜索值 → 和快照比对。
调用关系:它保护快捷键列表的搜索体验,防止重构时丢掉 search_value。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert_snapshot!, default, selection_tab)。
tests::picker_wide_render_snapshot1201–1206 ↗
fn picker_wide_render_snapshot()
作用:保存宽屏下快捷键选择器的显示快照。
数据流:生成默认选择器 → 用 120 列宽渲染 → 和快照比对。
调用关系:它关注宽屏布局,发现界面排版变化。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 2 个(assert_snapshot!, default)。
tests::picker_narrow_render_snapshot1209–1214 ↗
fn picker_narrow_render_snapshot()
作用:保存窄屏下快捷键选择器的显示快照。
数据流:生成默认选择器 → 用 78 列宽渲染 → 和快照比对。
调用关系:它关注小窗口布局,保证终端较窄时仍可读。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 2 个(assert_snapshot!, default)。
tests::picker_custom_render_snapshot1217–1225 ↗
fn picker_custom_render_snapshot()
作用:保存有自定义快捷键时选择器的显示快照。
数据流:先设置一个自定义绑定 → 生成运行时表和选择器 → 渲染并比对快照。
调用关系:它验证自定义配置在界面上的展示不会意外变化。
调用图:调用 3 个内部函数(from_config, keymap_with_replacement, build_keymap_picker_params);外部调用 2 个(assert_snapshot!, default)。
tests::picker_narrow_uses_compact_tabs1228–1238 ↗
fn picker_narrow_uses_compact_tabs()
作用:确认窄屏时选择器会用更紧凑的显示方式。
数据流:生成默认选择器并以窄宽度渲染 → 检查应出现的核心内容和不应出现的详细说明。
调用关系:它保护窄屏适配逻辑,避免界面塞入过多文字。
调用图:调用 2 个内部函数(defaults, build_keymap_picker_params);外部调用 3 个(assert!, default, render_picker)。
tests::capture_view_snapshot1335–1349 ↗
fn capture_view_snapshot()
作用:保存按键捕获视图初始显示的快照。
数据流:直接创建 KeymapCaptureView → 渲染成缓冲区调试文本 → 和快照比对。
调用关系:它保护 KeymapCaptureView::lines 和 render 的可见文案。
调用图:调用 1 个内部函数(new);外部调用 2 个(assert_snapshot!, app_event_sender)。
tests::debug_view_initial_snapshot1352–1359 ↗
fn debug_view_initial_snapshot()
作用:保存按键调试视图刚打开时的快照。
数据流:创建默认调试视图 → 渲染成字符串 → 和快照比对。
调用关系:它覆盖 build_keymap_debug_view 的初始界面。
调用图:调用 2 个内部函数(defaults, build_keymap_debug_view);外部调用 2 个(assert_snapshot!, default)。
tests::debug_view_shows_delayed_missing_key_hint1362–1369 ↗
fn debug_view_shows_delayed_missing_key_hint()
作用:确认调试视图等待一段时间后会提示用户可能没有收到按键。
数据流:创建调试视图 → 手动触发延迟提示 → 渲染并检查包含 Still waiting → 和快照比对。
调用关系:它验证诊断界面对“没收到按键”的友好提示。
调用图:调用 2 个内部函数(defaults, build_keymap_debug_view);外部调用 4 个(assert!, assert_snapshot!, default, render_debug)。
tests::debug_view_reports_detected_key_and_matching_actions1372–1381 ↗
fn debug_view_reports_detected_key_and_matching_actions()
作用:确认调试视图能显示检测到的按键以及匹配到的动作。
数据流:创建调试视图并显示延迟提示 → 模拟 Ctrl+O → 渲染 → 检查延迟提示消失并比对快照。
调用关系:它验证调试视图的按键接收和匹配展示流程。
调用图:调用 2 个内部函数(defaults, build_keymap_debug_view);外部调用 6 个(Char, new, assert!, assert_snapshot!, default, render_debug)。
tests::debug_view_uses_custom_binding_source1384–1395 ↗
fn debug_view_uses_custom_binding_source()
作用:确认调试视图能标出某个匹配动作来自自定义绑定。
数据流:把 global.copy 改成 ctrl-x → 创建运行时表和调试视图 → 模拟 Ctrl+X → 检查显示动作和 [Custom] 标记。
调用关系:它验证自定义来源信息能从配置一路传到调试界面。
调用图:调用 3 个内部函数(from_config, build_keymap_debug_view, keymap_with_replacement);外部调用 5 个(Char, new, assert!, default, render_debug)。
tests::debug_view_labels_custom_global_fallback_source1398–1409 ↗
fn debug_view_labels_custom_global_fallback_source()
作用:确认调试视图能区分“自定义的全局兜底绑定”。
数据流:在 global.queue 写入 ctrl-q → 生成运行时表和调试视图 → 模拟 Ctrl+Q → 检查显示 composer.queue 和 [Custom global]。
调用关系:它保护全局快捷键兜底来源的显示,避免和普通自定义混淆。
调用图:调用 2 个内部函数(from_config, build_keymap_debug_view);外部调用 7 个(Char, new, assert!, new, One, default, render_debug)。
tests::capture_completion_returns_to_selected_keymap_picker_row1412–1484 ↗
fn capture_completion_returns_to_selected_keymap_picker_row()
作用:测试完整的捕获成功流程:打开菜单、捕获新键、返回主列表并选中刚改的行。
数据流:创建测试面板 → 打开 picker 和动作菜单 → 模拟 Enter 打开捕获 → 模拟 Ctrl+Shift+K → 收到 KeymapCaptured → 生成新配置并刷新 picker → 检查焦点和关闭行为。
调用关系:它把 build_keymap_action_menu_params、build_keymap_capture_view、keymap_with_replacement 和 picker 刷新流程串起来验证。
调用图:调用 7 个内部函数(defaults, from_config, build_keymap_action_menu_params, build_keymap_capture_view, keymap_with_replacement, build_keymap_picker_params, build_keymap_picker_params_for_selected_action);外部调用 8 个(new, Char, new, assert!, assert_eq!, default, panic!, test_pane)。
tests::clear_completion_returns_to_selected_keymap_picker_row1487–1544 ↗
fn clear_completion_returns_to_selected_keymap_picker_row()
作用:测试清除自定义绑定后,界面会回到主快捷键列表并选中对应动作。
数据流:先创建自定义绑定 → 打开 picker 和动作菜单 → 选择删除 → 收到 KeymapCleared → 用默认配置刷新 picker → 检查焦点和关闭行为。
调用关系:它验证删除流程和成功后替换旧弹窗栈的行为。
调用图:调用 6 个内部函数(defaults, from_config, build_keymap_action_menu_params, keymap_with_replacement, build_keymap_picker_params, build_keymap_picker_params_for_selected_action);外部调用 6 个(new, assert!, assert_eq!, default, panic!, test_pane)。
tests::key_capture_serializes_modifier_order_for_config1594–1604 ↗
fn key_capture_serializes_modifier_order_for_config()
作用:确认捕获到带多个修饰键的大写字母时,配置字符串顺序稳定。
数据流:构造 Ctrl+Alt+K 事件 → 调用转换 → 断言得到 ctrl-alt-shift-k。
调用关系:它覆盖 key_event_to_config_key_spec 对大写字母自动加入 Shift 的规则。
调用图:外部调用 3 个(Char, new, assert_eq!)。
tests::key_capture_serializes_special_keys1607–1612 ↗
fn key_capture_serializes_special_keys()
作用:确认特殊键可以转换成配置字符串。
数据流:构造 Shift+PageDown 事件 → 调用转换 → 断言得到 shift-page-down。
调用关系:它验证 key_parts_to_config_key_spec 对非字母键的命名规则。
调用图:外部调用 1 个(assert_eq!)。
tests::key_capture_serializes_function_keys_through_f241615–1628 ↗
fn key_capture_serializes_function_keys_through_f24()
作用:确认功能键只支持 F1 到 F24。
数据流:分别转换 F13、F24、F25 → 前两个成功,F25 返回错误 → 和预期比较。
调用关系:它保护 MAX_FUNCTION_KEY 限制,避免写入终端不一定支持的功能键。
调用图:外部调用 1 个(assert_eq!)。
tests::key_capture_serializes_c0_control_chars_as_ctrl_bindings1631–1653 ↗
fn key_capture_serializes_c0_control_chars_as_ctrl_bindings()
作用:确认一些终端发来的控制字符会被规范化成 Ctrl 组合键。
数据流:构造换行、Ctrl+U、Ctrl+P 对应控制字符事件 → 转换 → 断言得到 ctrl-j、ctrl-u、ctrl-p。
调用关系:它覆盖 normalize_key_parts 的效果,保证不同终端发法能落成统一配置。
调用图:外部调用 1 个(assert_eq!)。
tests::key_capture_serializes_minus_as_named_key1656–1672 ↗
fn key_capture_serializes_minus_as_named_key()
作用:确认减号键在配置里写成 minus,避免和连接符混淆。
数据流:构造普通减号、Alt+减号、Ctrl+Alt+减号 → 转换 → 检查得到 minus、alt-minus、ctrl-alt-minus。
调用关系:它保护 key_parts_to_config_key_spec 对减号的特殊处理。
调用图:外部调用 1 个(assert_eq!)。
tests::replacement_sets_single_binding1675–1686 ↗
fn replacement_sets_single_binding()
作用:确认直接替换快捷键会写成单个绑定格式。
数据流:调用 keymap_with_replacement 设置 composer.submit → 检查配置字段是 One(ctrl-enter)。
调用关系:它验证 keymap_with_bindings 在一个键时使用单值结构。
调用图:调用 1 个内部函数(keymap_with_replacement);外部调用 2 个(assert_eq!, default)。
tests::replace_all_collapses_multi_binding_to_single1689–1723 ↗
fn replace_all_collapses_multi_binding_to_single()
作用:确认“替换全部”会把多个旧绑定压成一个新绑定。
数据流:先构造两个 submit 绑定 → 用 keymap_with_edit ReplaceAll → 检查返回绑定列表和配置都只剩新键。
调用关系:它覆盖 keymap_with_edit 的 ReplaceAll 分支。
调用图:调用 3 个内部函数(from_config, keymap_with_bindings, keymap_with_edit);外部调用 3 个(assert_eq!, default, panic!)。
tests::add_alternate_grows_single_binding1726–1754 ↗
fn add_alternate_grows_single_binding()
作用:确认给单个默认快捷键添加备用键时,会保留默认键并加上新键。
数据流:用默认运行时表 → 对 composer.submit 添加 ctrl-enter → 检查配置变成 enter 和 ctrl-enter 两个绑定。
调用关系:它验证 AddAlternate 会先把默认绑定物化到用户配置里,再追加新键。
调用图:调用 2 个内部函数(defaults, keymap_with_edit);外部调用 3 个(assert_eq!, default, panic!)。
tests::add_alternate_grows_default_multi_binding1757–1786 ↗
fn add_alternate_grows_default_multi_binding()
作用:确认给本来就有多个默认绑定的动作添加备用键时,所有旧键都会保留。
数据流:用默认运行时表 → 给 editor.move_left 添加 ctrl-shift-b → 检查 left、ctrl-b 和新键都写入配置。
调用关系:它覆盖 AddAlternate 对默认多绑定动作的处理。
调用图:调用 2 个内部函数(defaults, keymap_with_edit);外部调用 3 个(assert_eq!, default, panic!)。
tests::add_alternate_duplicate_is_noop1789–1807 ↗
fn add_alternate_duplicate_is_noop()
作用:确认添加一个已经存在的备用键不会改配置。
数据流:用默认运行时表 → 尝试给 composer.submit 添加 enter → 检查返回 Unchanged 和说明文字。
调用关系:它保护 keymap_with_edit 的重复检测,避免无意义保存。
调用图:调用 2 个内部函数(defaults, keymap_with_edit);外部调用 2 个(assert_eq!, default)。
tests::replace_one_preserves_other_bindings1810–1847 ↗
fn replace_one_preserves_other_bindings()
作用:确认只替换一个绑定时,其他绑定不受影响。
数据流:构造两个 submit 绑定 → 用 ReplaceOne 把 ctrl-enter 换成 ctrl-shift-enter → 检查另一个 alt-shift-enter 仍存在。
调用关系:它覆盖 keymap_with_edit 的 ReplaceOne 正常路径。
调用图:调用 3 个内部函数(from_config, keymap_with_bindings, keymap_with_edit);外部调用 3 个(assert_eq!, default, panic!)。
tests::replace_one_deduplicates_replacement1850–1886 ↗
fn replace_one_deduplicates_replacement()
作用:确认替换成另一个已经存在的绑定时,会自动去重。
数据流:构造 ctrl-enter 和 ctrl-shift-enter 两个绑定 → 把 ctrl-enter 替换成 ctrl-shift-enter → 检查最终只剩一个。
调用关系:它验证 ReplaceOne 分支会调用 dedup_bindings。
调用图:调用 3 个内部函数(from_config, keymap_with_bindings, keymap_with_edit);外部调用 3 个(assert_eq!, default, panic!)。
tests::replace_one_rejects_stale_old_key1889–1905 ↗
fn replace_one_rejects_stale_old_key()
作用:确认如果用户想替换的旧键已经不再生效,会报错而不是乱改。
数据流:用默认运行时表 → 尝试替换不存在的 alt-enter → 检查错误里包含动作和旧键。
调用关系:它保护过期菜单场景,避免配置在用户没意识到的情况下被覆盖。
调用图:调用 2 个内部函数(defaults, keymap_with_edit);外部调用 2 个(assert!, default)。
tests::clear_removes_custom_binding1908–1923 ↗
fn clear_removes_custom_binding()
作用:确认清除自定义绑定会把配置槽位恢复成 None。
数据流:先设置 composer.submit 自定义绑定 → 检查有自定义 → 调用 keymap_without_custom_binding → 检查字段为 None 且不再有自定义。
调用关系:它验证清除流程的底层配置改动。
调用图:调用 2 个内部函数(keymap_with_replacement, keymap_without_custom_binding);外部调用 2 个(assert_eq!, default)。
tests::replacement_rejects_unknown_action1926–1931 ↗
fn replacement_rejects_unknown_action()
作用:确认给不存在的动作设置快捷键会返回错误。
数据流:调用 keymap_with_replacement 设置 composer.nope → 得到错误 → 检查错误包含这个动作名。
调用关系:它保护 binding_slot 的校验路径,防止未知动作静默写坏配置。
调用图:调用 1 个内部函数(keymap_with_replacement);外部调用 2 个(assert!, default)。
tui/src/keymap_setup/debug.rs源码 ↗
这个文件实现了一个底部弹出的调试界面,专门帮人排查快捷键。用户打开它后,界面会提示“按任意键”。每按一次键,它就把几件事展示出来:Codex 识别到的按键名字、这个键能不能写进配置文件、终端发来的原始按键事件、以及当前键位表里有哪些动作会被它触发。如果一直没有收到按键,它还会过几秒换成更明确的提示,告诉用户可能是终端没有发送这个键。它像一个“快递验货窗口”:不是直接改快捷键,而是先把终端送来的包裹打开给你看清楚。Ctrl+C 会关闭这个面板,Esc 不会立刻退出,而是也会被拿来检查。
build_keymap_debug_view44–55 ↗
fn build_keymap_debug_view(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
) -> KeymapDebugView
作用:创建一个新的按键调试面板。调用者把当前正在使用的快捷键表和配置交给它,它会保存一份,用来之后判断用户按下的键会匹配哪些动作。
数据流:进去的是运行时快捷键表和配置里的快捷键设置 → 它把两者复制下来,并记录当前打开时间,同时把“最近一次按键报告”设为空、完成状态设为未完成 → 出来的是一个可以显示、可以接收按键的 KeymapDebugView。
调用关系:这个函数是打开调试面板时的入口。测试里的多个场景会用它先造出面板,然后再检查初始显示、按键报告、延迟提示和自定义快捷键来源是否正确。
调用图:被 5 处调用(debug_view_initial_snapshot, debug_view_labels_custom_global_fallback_source, debug_view_reports_detected_key_and_matching_actions, debug_view_shows_delayed_missing_key_hint, debug_view_uses_custom_binding_source);外部调用 3 个(now, clone, clone)。
KeymapDebugView::lines58–60 ↗
fn lines(&self, width: u16) -> Vec<Line<'static>>
作用:生成当前界面要显示的所有文字行。它使用“现在这一刻”的时间,所以能决定是否该显示等待太久后的额外提示。
数据流:进去的是可用宽度 → 它读取面板里的状态,比如打开时间和最近按键报告,并取得当前时间 → 出来的是一组已经组织好的文本行,供绘制和计算高度使用。
调用关系:它是渲染前的文字准备步骤。render 用它拿到要画的内容,desired_height 也用它计算这个面板需要占几行。具体排版工作交给 KeymapDebugView::lines_at。
调用图:调用 1 个内部函数(lines_at);被 2 处调用(desired_height, render);外部调用 1 个(now)。
KeymapDebugView::lines_at62–130 ↗
fn lines_at(&self, width: u16, now: Instant) -> Vec<Line<'static>>
作用:按指定时间生成完整的调试界面文字。这个函数把标题、提示、按键结果、原始事件和匹配到的动作整理成适合终端显示的行。
数据流:进去的是显示宽度和一个时间点 → 它先放标题和说明,再根据是否等待超过 3 秒选择短提示或长提示;如果还没有按键,就显示等待中;如果已有按键报告,就展示检测结果、配置写法、原始事件和匹配到的动作 → 出来的是一组可直接渲染的文字行。
调用关系:这是整个面板显示内容的核心。KeymapDebugView::lines 会调用它;它会询问 KeymapDebugView::should_show_delayed_hint 是否该显示长提示,并把过长文字交给 push_wrapped_dim 自动换行。
调用图:调用 2 个内部函数(should_show_delayed_hint, push_wrapped_dim);被 1 处调用(lines);外部调用 4 个(from, format!, from, vec!)。
KeymapDebugView::should_show_delayed_hint132–134 ↗
fn should_show_delayed_hint(&self, now: Instant) -> bool
作用:判断要不要显示更详细的“终端可能没发按键”提示。只有在还没收到任何按键,并且面板已经打开至少 3 秒时,才会显示。
数据流:进去的是当前时间 → 它读取面板打开的时间和是否已有按键报告,计算已经等了多久 → 出来的是 true 或 false,表示是否该换成长提示。
调用关系:它服务于 KeymapDebugView::lines_at。显示文字时,lines_at 先问它一句“用户是不是等太久了”,再决定用短提示还是长提示。
调用图:被 1 处调用(lines_at);外部调用 1 个(duration_since)。
KeymapDebugView::show_delayed_hint_for_test137–139 ↗
fn show_delayed_hint_for_test(&mut self)
作用:这是给测试用的小开关,用来假装这个面板已经打开很久了。这样测试不用真的等 3 秒,也能检查延迟提示是否会出现。
数据流:进去的是这个面板本身 → 它把打开时间改成“现在减去延迟时间” → 出来的效果是下一次生成界面文字时,会认为已经到了显示长提示的时候。
调用关系:它只在测试配置下存在。测试会调用它制造“已经等待足够久”的状态,然后再检查界面是不是显示了正确的提示。
调用图:外部调用 1 个(now)。
KeymapDebugView::render143–145 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:把按键调试面板真正画到终端界面上。它不负责决定内容,只负责把已经生成好的文字放进指定区域。
数据流:进去的是要绘制的屏幕区域和屏幕缓冲区(一块还没最终显示出来的画布) → 它根据区域宽度生成文字行,再包装成段落控件画进去 → 出来的是被写入内容的缓冲区,之后终端会显示这些内容。
调用关系:它是界面绘制流程调用的函数。外部的 render_debug 会需要它;它内部把内容生成交给 KeymapDebugView::lines,再交给界面库的 Paragraph 去画。
调用图:调用 1 个内部函数(lines);被 1 处调用(render_debug);外部调用 1 个(new)。
KeymapDebugView::desired_height147–149 ↗
fn desired_height(&self, width: u16) -> u16
作用:告诉外层界面这个面板理想情况下需要多高。因为文字会按宽度换行,所以宽度不同,需要的行数也可能不同。
数据流:进去的是可用宽度 → 它生成当前宽度下的所有显示行,并数一共有多少行 → 出来的是一个高度数字。
调用关系:它在布局时被使用,比如 render_debug 需要知道该给底部面板留多少空间。它不自己排版,而是复用 KeymapDebugView::lines 的结果。
调用图:调用 1 个内部函数(lines);被 1 处调用(render_debug)。
KeymapDebugView::handle_key_event153–168 ↗
fn handle_key_event(&mut self, key_event: KeyEvent)
作用:处理用户按下的键,并生成一份新的按键检查报告。它会忽略按键释放事件,因为这里关心的是“按下时终端发来了什么”。
数据流:进去的是一个终端按键事件 → 如果这是松开键的事件,它什么都不改;否则它把事件转成可读的按键标签,尝试转成配置文件里的写法,生成原始事件摘要,并查找当前快捷键表里会被触发的动作 → 出来的是保存到面板里的 last_report,下一次渲染就会显示这份报告。
调用关系:它是用户按键后的核心处理点。底部面板框架把按键交给它;它会借助 KeyBinding::from_event、key_event_to_config_key_spec、key_event_debug_summary 和 matching_actions_for_key_event,把一个按键变成完整报告。
调用图:调用 3 个内部函数(from_event, matching_actions_for_key_event, key_event_debug_summary);外部调用 1 个(key_event_to_config_key_spec)。
KeymapDebugView::is_complete170–172 ↗
fn is_complete(&self) -> bool
作用:告诉外层这个调试面板是否已经结束。结束后,外层就可以把这个底部面板关掉。
数据流:进去的是面板当前状态 → 它读取 complete 这个标记 → 出来的是 true 或 false,表示是否完成。
调用关系:它被底部面板框架用来判断生命周期。KeymapDebugView::on_ctrl_c 会把 complete 改成 true,之后这个函数就会告诉外层可以收起面板。
KeymapDebugView::on_ctrl_c174–177 ↗
fn on_ctrl_c(&mut self) -> CancellationEvent
作用:处理 Ctrl+C。这里 Ctrl+C 的意思是关闭按键检查器,而不是继续把它当作普通按键来检查。
数据流:进去的是可修改的面板状态 → 它把 complete 设为 true → 出来的是 CancellationEvent::Handled,表示这个取消动作已经被面板自己处理了。
调用关系:它由底部面板框架在用户按 Ctrl+C 时调用。它负责结束这个调试面板,并通过返回值告诉外层不用再额外处理这次 Ctrl+C。
KeymapDebugView::prefer_esc_to_handle_key_event179–181 ↗
fn prefer_esc_to_handle_key_event(&self) -> bool
作用:声明 Esc 键应该交给这个面板检查,而不是被外层当作“退出”快捷键优先处理。这样用户才能看见终端发送的 Esc 到底长什么样。
数据流:进去的是面板状态,但它不需要读取任何内容 → 它固定返回 true → 出来的效果是 Esc 会进入 handle_key_event,被当作普通待检查按键。
调用关系:它被底部面板框架用来决定 Esc 的归属。因为这个面板就是检查按键的,所以它要求优先拿到 Esc 事件。
KeymapDebugView::next_frame_delay183–192 ↗
fn next_frame_delay(&self) -> Option<Duration>
作用:告诉界面下一次自动刷新应该等多久。它主要是为了在没有按键时,到了 3 秒能自动把短提示换成长提示。
数据流:进去的是面板当前状态 → 如果已经有按键报告,它返回空,表示不需要为提示再安排刷新;如果还没按键,它计算距离“显示长提示的时间点”还差多久 → 出来的是一个等待时长,或者空值。
调用关系:它服务于界面的刷新调度。打开面板后,如果用户一直不按键,外层可以根据它安排下一帧;等时间到了,KeymapDebugView::lines_at 就会显示更详细的提示。
调用图:外部调用 1 个(checked_add)。
push_wrapped_dim195–210 ↗
fn push_wrapped_dim(
lines: &mut Vec<Line<'static>>,
text: String,
wrap_width: usize,
initial_indent: &'static str,
subsequent_indent: &'static str,
)
作用:把一段可能很长的说明文字拆成多行,并用灰暗样式显示。这样窄窗口里文字不会挤成一团,也能保持缩进好看。
数据流:进去的是要追加到的行列表、一段文字、可用宽度、第一行缩进和后续行缩进 → 它按宽度自动换行,每一行都套上灰暗样式,再追加到行列表里 → 出来的是变长后的行列表。
调用关系:它是 KeymapDebugView::lines_at 的排版帮手。凡是提示、原始事件或动作描述可能太长时,lines_at 都会把文字交给它处理。
key_event_debug_summary212–219 ↗
fn key_event_debug_summary(key_event: KeyEvent) -> String
作用:把一个终端按键事件压缩成适合人看的原始摘要。它会显示按键代码、修饰键和事件类型,方便排查终端实际发来了什么。
数据流:进去的是一个 KeyEvent,也就是终端发来的按键事件 → 它取出 code、modifiers 和 kind,并把修饰键整理成可读文字 → 出来的是类似“code=..., modifiers=..., kind=...”的字符串。
调用关系:它由 KeymapDebugView::handle_key_event 调用,用来填充报告里的 Raw event 一栏。用户看到这一栏,就能知道 Codex 收到的底层按键信息。
调用图:被 1 处调用(handle_key_event);外部调用 1 个(format!)。
key_modifiers_debug_label221–243 ↗
fn key_modifiers_debug_label(modifiers: KeyModifiers) -> String
作用:把修饰键组合变成人能读懂的标签。修饰键就是 Ctrl、Alt、Shift 这类和其他键一起按的键。
数据流:进去的是一组修饰键标记 → 如果没有任何修饰键,就返回“none”;否则它依次检查 Ctrl、Alt、Shift,并把不认识的额外标记也保留下来 → 出来的是用竖线连接的文字,比如“ctrl|shift”。
调用关系:它是生成按键原始摘要时的辅助工具。它把机器内部的修饰键标记翻译成用户能看懂的字符串,让调试报告更直观。
tui/src/keymap_setup/actions.rs源码 ↗
可以把这个文件想成一张“快捷键菜单的菜谱”。每道菜就是一个可配置动作,比如提交输入、复制回复、滚动列表、批准请求。KEYMAP_ACTIONS 是公开给界面看的清单,里面写了动作属于哪个区域、给用户看的名字、稳定的配置名和一句说明。然后几个查询函数把这张清单和两类数据接起来:一类是用户配置文件里的原始设置,另一类是程序运行时已经算好的真实快捷键。这样 /keymap 页面既能显示“当前是什么键”,也能修改“配置里该写哪一项”。文件还支持调试:用户按下一个键时,它能反查这个键命中了哪些动作,并标出来源是自定义、全局自定义,还是默认值。少了它,快捷键界面很容易漏动作、显示错键,或者改错配置位置。
action37–50 ↗
fn action(
context: &'static str,
context_label: &'static str,
action: &'static str,
description: &'static str,
) -> KeymapActionDescriptor
作用:创建一个普通的快捷键动作说明。它用于把“配置里的名字”和“用户看得懂的说明”打包成一条目录项。
数据流:进去的是动作所在区域、区域显示名、动作名和说明文字 → 它把这些值放进 KeymapActionDescriptor,并标记为不需要额外功能开关 → 出来的是一条可放进动作总表的描述记录,不改动外部数据。
调用关系:它主要服务于 KEYMAP_ACTIONS 这张动作总表。表里的大多数动作都通过它创建,后面的菜单显示、绑定查询和按键调试都依赖这些描述记录。
gated_action52–66 ↗
fn gated_action(
context: &'static str,
context_label: &'static str,
action: &'static str,
description: &'static str,
required_feature: KeymapActionFeature,
) -> KeymapActionDescri
作用:创建一个“有条件才显示”的快捷键动作说明。比如某个功能没开启时,相关快捷键就不应该出现在 /keymap 里。
数据流:进去的是区域、显示名、动作名、说明文字,以及需要的功能开关 → 它生成一条描述记录,并把“需要哪个功能”一起记下来 → 出来的是带显示条件的动作描述,不直接检查当前是否开启。
调用关系:它也用于构建 KEYMAP_ACTIONS。之后 KeymapActionDescriptor::is_visible 会根据当前过滤条件判断这类动作要不要给用户看。
KeymapActionDescriptor::is_visible79–84 ↗
fn is_visible(self, filter: KeymapActionFilter) -> bool
作用:判断某个动作现在应不应该出现在快捷键设置界面里。它避免把用户当前不能用的功能也拿出来让人配置。
数据流:进去的是一个动作描述和一个过滤条件,比如是否开启 Fast mode → 它查看这条动作有没有要求特定功能 → 出来的是 true 或 false,表示显示或隐藏,不改动动作本身。
调用关系:它站在动作目录和界面展示之间。界面准备列出动作时,可以先用它筛一遍,只有满足当前功能开关的动作才会进入菜单。
action_label205–217 ↗
fn action_label(action: &str) -> String
作用:把配置里稳定但偏机器化的动作名,变成用户更容易读的菜单标题。比如把 open_transcript 变成 Open Transcript。
数据流:进去的是一个用下划线连接的动作名字符串 → 它按下划线拆成单词,把每个单词首字母变成大写,再用空格连起来 → 出来的是展示用文字;原始动作名不会被改。
调用关系:它被 build_keymap_action_menu_params、build_keymap_capture_view 和 build_keymap_replace_binding_menu_params 使用。也就是说,凡是快捷键菜单或捕获界面需要显示动作标题时,都会找它把内部名字翻译成人能看的名字。
调用图:被 3 处调用(build_keymap_action_menu_params, build_keymap_capture_view, build_keymap_replace_binding_menu_params)。
binding_slot226–343 ↗
fn binding_slot(
keymap: &'a mut TuiKeymap,
context: &str,
action: &str,
) -> Option<&'a mut Option<KeybindingsSpec>>
作用:根据“区域 + 动作名”,找到用户配置里对应的那个快捷键设置位置。它的关键作用是让编辑器知道该改配置文件里的哪一格。
数据流:进去的是一份可修改的 TuiKeymap 配置、区域名和动作名 → 它用一大张匹配表找到对应字段,例如 composer 的 submit → 出来的是那个字段的可修改引用;如果找不到,就返回空。这个字段本身可能是未设置、自定义绑定,或显式解绑。
调用关系:它被 debug_binding_source、has_custom_binding、keymap_with_bindings 和 keymap_without_custom_binding 使用。换句话说,检查是否自定义、写入新绑定、删除自定义绑定、判断调试来源时,都要先通过它定位配置位置。
调用图:被 4 处调用(debug_binding_source, has_custom_binding, keymap_with_bindings, keymap_without_custom_binding)。
bindings_for_action351–468 ↗
fn bindings_for_action(
runtime_keymap: &'a RuntimeKeymap,
context: &str,
action: &str,
) -> Option<&'a [KeyBinding]>
作用:根据“区域 + 动作名”,读取程序运行时真正生效的快捷键。它回答的是“现在按哪些键会触发这个动作”,不是“配置文件里写了什么”。
数据流:进去的是已经解析好的 RuntimeKeymap、区域名和动作名 → 它从运行时快捷键表里找到对应动作的按键列表 → 出来的是一段只读的 KeyBinding 列表;如果动作名不认识,就返回空。
调用关系:它被 active_binding_specs 和 build_keymap_capture_view 调用。快捷键界面需要显示当前有效按键,或把当前绑定转成配置文本时,会通过它读取运行时结果。
调用图:被 2 处调用(active_binding_specs, build_keymap_capture_view)。
format_binding_summary475–487 ↗
fn format_binding_summary(bindings: &[KeyBinding]) -> String
作用:把一个动作的快捷键列表压缩成适合菜单显示的一行文字。没有绑定时显示 unbound,有多个键时用逗号隔开。
数据流:进去的是若干个运行时按键绑定 → 它把每个绑定转换成配置里能写的按键文本,并去掉看起来重复的等价写法 → 出来的是一段简短字符串,比如 Ctrl+C, Esc 或 unbound,不改动原绑定。
调用关系:它会调用集合创建和遍历这些基础操作来去重。build_keymap_capture_view 会用它显示某个动作当前绑定了哪些键,让用户不用看复杂的内部按键结构。
调用图:被 1 处调用(build_keymap_capture_view);外部调用 2 个(new, iter)。
KeymapDebugBindingSource::label497–503 ↗
fn label(&self) -> &'static str
作用:把调试用的绑定来源变成可显示的短标签。比如显示成 Custom、Custom global 或 Default。
数据流:进去的是一个来源枚举值,也就是程序内部用来区分来源的小标签 → 它按不同来源返回固定英文文字 → 出来的是界面能直接显示的字符串,不改动任何状态。
调用关系:它服务于按键调试展示。前面的逻辑先判断一个命中的动作来自哪里,界面需要展示时再用它把内部分类变成用户能读的文字。
matching_actions_for_key_event515–537 ↗
fn matching_actions_for_key_event(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
event: KeyEvent,
) -> Vec<KeymapDebugActionMatch>
作用:反查“用户刚按下的这个键,会触发哪些动作”。这对调试快捷键冲突和理解当前键位非常有用。
数据流:进去的是运行时快捷键表、用户原始配置和一次按键事件 → 它遍历动作总表,读取每个动作的有效绑定,检查是否有绑定匹配这次按键 → 出来的是命中的动作列表,每条包含区域、动作名、显示标签、说明和绑定来源。
调用关系:它被 handle_key_event 调用,通常是在处理真实按键时用来做调试或诊断。它内部会用 bindings_for_action 找有效按键,用 action_label 做显示名,并用 debug_binding_source 判断来源。
调用图:被 1 处调用(handle_key_event)。
debug_binding_source539–559 ↗
fn debug_binding_source(
keymap_config: &TuiKeymap,
descriptor: &KeymapActionDescriptor,
) -> KeymapDebugBindingSource
作用:判断一个动作的快捷键来源:是这个动作自己被用户改过、是借用了全局自定义设置,还是完全来自默认值。
数据流:进去的是用户快捷键配置和一个动作描述 → 它先复制一份配置,避免为了查字段而改到原件;再用 binding_slot 看动作自身有没有自定义;如果没有,再用 global_fallback_slot 看是否有可用的全局兜底配置 → 出来的是来源分类。
调用关系:它被 matching_actions_for_key_event 使用。按键命中某个动作后,需要告诉用户“为什么这个键会触发它”,这个函数就负责给出来源解释。
调用图:调用 2 个内部函数(binding_slot, global_fallback_slot);外部调用 1 个(clone)。
global_fallback_slot561–575 ↗
fn global_fallback_slot(
keymap: &'a mut TuiKeymap,
descriptor: &KeymapActionDescriptor,
) -> Option<&'a mut Option<KeybindingsSpec>>
作用:找到少数 composer 动作对应的全局兜底快捷键设置。也就是说,如果局部没有配置,有些输入框动作可以退回去用全局配置。
数据流:进去的是可修改的 TuiKeymap 和动作描述 → 它先确认动作区域是不是 composer,不是就直接返回空;是的话,只对 submit、queue、toggle_shortcuts 这几个动作返回对应的全局配置位置 → 出来的是可选的配置字段引用。
调用关系:它只被 debug_binding_source 调用。调试来源时,如果动作自身没有自定义绑定,就用它检查是否命中了这种“全局兜底”的特殊规则。
调用图:被 1 处调用(debug_binding_source)。
tui/src/keymap_setup/picker.rs源码 ↗
这个文件像是在给快捷键做一张清楚的菜单表。它先从运行中的快捷键表和用户配置里,整理出每个动作的名字、所属场景、说明、当前按键,以及是不是用户自己改过。然后它把这些动作分成多个标签页,比如全部、常用、自定义、未绑定、编辑器、审批等,让用户不用在一大堆项目里硬找。每一行还会带一个小标记:星号表示自定义,减号表示没有绑定按键。用户按回车选中某一行时,会发出打开该动作编辑菜单的事件。文件里还放了一个调试标签页,用来查看终端实际传来的按键是什么,方便排查“我按了这个键,程序为什么没识别”的问题。
KeymapActionRow::is_unbound49–51 ↗
fn is_unbound(&self) -> bool
作用:判断这一行快捷键是不是“没有绑定任何按键”。这让界面能把未绑定的动作单独挑出来,或者用特殊符号标出来。
数据流:输入是一条快捷键行里的绑定摘要文字 → 它检查这段文字是否正好是 unbound → 输出真假值,不改动任何数据。
调用关系:它是行状态的小判断工具。keymap_row_prefix 会用它决定这一行前面该显示减号还是空白;构建整体页面时也会用同样的判断思路统计未绑定数量。
调用图:被 1 处调用(keymap_row_prefix)。
build_keymap_picker_params125–134 ↗
fn build_keymap_picker_params(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
) -> SelectionViewParams
作用:用默认筛选条件创建完整的快捷键选择器参数。测试里常用它来快速拿到一个正常的 /keymap 页面结构。
数据流:输入是当前运行中的快捷键表和用户配置 → 它补上默认的动作筛选条件 → 交给更通用的构建函数继续处理 → 输出一份可直接显示的选择器页面参数。
调用关系:它是一个省事入口,主要服务测试场景。它不自己拼页面,而是把活交给 build_keymap_picker_params_with_filter,让后面的统一流程负责生成标签页和列表。
调用图:调用 1 个内部函数(build_keymap_picker_params_with_filter);被 15 处调用(capture_completion_returns_to_selected_keymap_picker_row, clear_completion_returns_to_selected_keymap_picker_row, picker_all_tab_items_remain_searchable, picker_approval_tab_lists_all_approval_actions, picker_common_tab_lists_curated_actions, picker_content_snapshot, picker_custom_render_snapshot, picker_customized_tab_contains_root_overrides, picker_debug_tab_is_last_and_opens_inspector, picker_hides_fast_mode_action_when_feature_is_disabled (+5 more));外部调用 1 个(default)。
build_keymap_picker_params_with_filter136–147 ↗
fn build_keymap_picker_params_with_filter(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
action_filter: KeymapActionFilter,
) -> SelectionViewParams
作用:按指定筛选条件创建快捷键选择器。比如某些功能开关没开时,可以把对应动作先过滤掉。
数据流:输入是运行时快捷键、配置文件里的快捷键设置,以及一个动作筛选器 → 它表示“不指定初始选中的动作” → 调用核心构建函数 → 输出选择器页面参数。
调用关系:它是正式代码会用到的入口之一,也被测试用来验证不同筛选条件下页面是否正确。它把实际页面拼装交给 build_keymap_picker_params_for_action。
调用图:调用 1 个内部函数(build_keymap_picker_params_for_action);被 4 处调用(build_keymap_picker_params, keymap_picker_fast_mode_enabled_snapshot, picker_covers_every_replaceable_action, picker_shows_fast_mode_action_when_feature_is_enabled)。
build_keymap_picker_params_for_selected_action150–163 ↗
fn build_keymap_picker_params_for_selected_action(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
context: &str,
action: &str,
) -> SelectionViewParams
作用:创建快捷键选择器,并让某个指定动作一开始就被选中。这样用户编辑完某个快捷键返回列表时,不会迷路。
数据流:输入是快捷键数据、用户配置、目标场景和目标动作名 → 它使用默认筛选条件 → 转交给带筛选版本 → 输出带初始选中行的选择器参数。
调用关系:它主要用于测试“返回页面后焦点还在原来那一行”等体验。它把具体工作交给 build_keymap_picker_params_for_selected_action_with_filter。
调用图:调用 1 个内部函数(build_keymap_picker_params_for_selected_action_with_filter);被 4 处调用(capture_completion_returns_to_selected_keymap_picker_row, clear_completion_returns_to_selected_keymap_picker_row, picker_selected_action_starts_on_matching_all_tab_row, replace_one_completion_drops_focused_keymap_submenus);外部调用 1 个(default)。
build_keymap_picker_params_for_selected_action_with_filter165–178 ↗
fn build_keymap_picker_params_for_selected_action_with_filter(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
action_filter: KeymapActionFilter,
context: &str,
action:
作用:在指定筛选条件下创建快捷键选择器,并尝试选中某个指定动作。它是“带筛选”和“带初始选中”两个需求的组合入口。
数据流:输入是快捷键数据、配置、筛选器、目标场景和目标动作 → 它把目标动作包装成可选值 → 调用核心构建函数 → 输出页面参数,里面可能带有初始选中位置。
调用关系:它由 build_keymap_picker_params_for_selected_action 调用,自己不拼界面,只负责把参数整理好,再交给 build_keymap_picker_params_for_action。
调用图:调用 1 个内部函数(build_keymap_picker_params_for_action);被 1 处调用(build_keymap_picker_params_for_selected_action)。
build_keymap_picker_params_for_action180–300 ↗
fn build_keymap_picker_params_for_action(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
action_filter: KeymapActionFilter,
selected_action: Option<(&str, &str)>,
) -> Sele
作用:这是本文件的核心装配函数,真正把快捷键数据做成一个能显示、能搜索、能分组、能点击的选择器页面。
数据流:输入是运行时快捷键、配置、筛选器,以及可选的目标动作 → 它先生成所有行,再统计总数、自定义数、未绑定数,计算初始选中行和列宽;随后组装全部、常用、自定义、未绑定、各场景分组和调试标签页 → 输出完整的 SelectionViewParams,也就是底部选择器需要的整套页面说明。
调用关系:上层几个公开构建函数最终都会来到这里。它把不同小零件串起来:build_keymap_rows 造原始行,keymap_common_rows 挑常用项,keymap_selection_items 把行变成可点击项目,keymap_header 和提示行函数负责显示说明,keymap_debug_tab 加入调试入口。
调用图:调用 7 个内部函数(action_count_line, build_keymap_rows, keymap_common_rows, keymap_debug_tab, keymap_header, keymap_picker_hint_line, keymap_selection_items);被 2 处调用(build_keymap_picker_params_for_selected_action_with_filter, build_keymap_picker_params_with_filter);外部调用 5 个(new, default, new, format!, vec!)。
keymap_debug_tab302–327 ↗
fn keymap_debug_tab() -> SelectionTab
作用:创建“Debug”调试标签页,用来检查终端实际传给程序的按键。这个功能对排查快捷键识别问题很有用。
数据流:它不需要外部输入 → 它生成一个标签页,里面只有一个“Inspect keypresses”项目;用户按回车时会发送打开按键调试器的事件 → 输出这个调试标签页。
调用关系:build_keymap_picker_params_for_action 在组装所有标签页时会把它追加进去。它自己用 keymap_header 做页头,并把后续动作交给应用事件系统处理。
调用图:调用 1 个内部函数(keymap_header);被 1 处调用(build_keymap_picker_params_for_action);外部调用 1 个(vec!)。
build_keymap_rows329–358 ↗
fn build_keymap_rows(
runtime_keymap: &RuntimeKeymap,
keymap_config: &TuiKeymap,
action_filter: KeymapActionFilter,
) -> Vec<KeymapActionRow>
作用:把系统知道的所有可配置动作,整理成一行一行适合显示的快捷键信息。
数据流:输入是运行中的快捷键表、用户配置和动作筛选器 → 它遍历动作清单,跳过不可见动作,查出每个动作当前绑定的按键,生成显示用名称和摘要,并判断是否有用户自定义绑定 → 输出 KeymapActionRow 列表。
调用关系:它是页面数据的源头,由 build_keymap_picker_params_for_action 调用。后面的常用列表、自定义列表、未绑定列表和各场景标签页,都是从它产出的这些行里再筛出来的。
调用图:被 1 处调用(build_keymap_picker_params_for_action)。
keymap_common_rows360–368 ↗
fn keymap_common_rows(rows: &[KeymapActionRow]) -> Vec<&KeymapActionRow>
作用:从全部快捷键行里挑出“常用快捷键”。这样用户能先看到最可能会改的那些键。
数据流:输入是所有快捷键行 → 它按照预先写好的常用动作顺序逐个查找匹配行 → 输出这些行的引用列表,顺序保持为常用清单的顺序。
调用关系:build_keymap_picker_params_for_action 用它生成 Common 标签页。它不创建新数据,只是在全部行里挑重点,像从厚通讯录里摘出常联系人。
调用图:被 1 处调用(build_keymap_picker_params_for_action)。
keymap_selection_items370–389 ↗
fn keymap_selection_items(
rows: impl IntoIterator<Item = &'a KeymapActionRow>,
empty_name: &str,
empty_description: &str,
) -> Vec<SelectionItem>
作用:把快捷键行变成选择器能显示的项目列表。如果没有项目,它会放一个灰掉的提示项,避免页面空白。
数据流:输入是一批快捷键行,以及空列表时要显示的标题和说明 → 它把每一行转换成 SelectionItem;如果转换后为空,就生成一个不可点击的占位项目 → 输出选择器项目列表。
调用关系:build_keymap_picker_params_for_action 在创建每个标签页时都会用它。它内部依赖单行转换逻辑,把原始快捷键行包装成底部选择器认识的格式。
调用图:被 1 处调用(build_keymap_picker_params_for_action);外部调用 2 个(into_iter, vec!)。
keymap_selection_item391–417 ↗
fn keymap_selection_item(row: &KeymapActionRow) -> SelectionItem
作用:把一条快捷键行变成一个可点击、可搜索的列表项目。用户选中它后,就能进入这个动作的快捷键编辑菜单。
数据流:输入是一条快捷键行 → 它准备显示名称、前缀标记、当前按键说明、搜索用文字,并创建一个点击动作;这个点击动作会发送打开动作菜单的事件,带上场景和动作名 → 输出一个 SelectionItem。
调用关系:它是 keymap_selection_items 处理每一行时用到的单行包装器。它会调用 keymap_row_prefix 生成左侧场景名和自定义/未绑定标记,后续真正打开菜单则交给应用事件系统。
调用图:调用 1 个内部函数(keymap_row_prefix);外部调用 3 个(default, format!, vec!)。
keymap_row_prefix419–438 ↗
fn keymap_row_prefix(row: &KeymapActionRow) -> Vec<Span<'static>>
作用:生成每一行左侧的小前缀,包括动作所属场景,以及自定义或未绑定的标记。这样用户扫一眼就知道这一行的大致状态。
数据流:输入是一条快捷键行 → 它先看是否是自定义绑定,是就用高亮星号;否则看是否未绑定,是就用暗色减号;普通项则留空 → 输出一组带样式的文字片段。
调用关系:keymap_selection_item 用它给列表项加左侧装饰。它会调用 KeymapActionRow::is_unbound 判断未绑定状态,并用统一的强调色函数保证界面风格一致。
调用图:调用 2 个内部函数(is_unbound, accent_style);被 1 处调用(keymap_selection_item);外部调用 1 个(vec!)。
keymap_header440–446 ↗
fn keymap_header(description: String, summary: String) -> Box<dyn Renderable>
作用:生成每个快捷键标签页顶部的说明区。它告诉用户当前页是什么,以及有多少动作之类的摘要信息。
数据流:输入是页面描述和摘要文字 → 它创建一个竖排文字块,第一行是加粗的 Keymap,后两行是变暗的说明和统计 → 输出一个可渲染的页头对象。
调用关系:build_keymap_picker_params_for_action 用它给各个普通标签页做页头,keymap_debug_tab 也用它给调试页做页头。它只负责显示外壳,不决定里面有哪些快捷键。
调用图:调用 1 个内部函数(new);被 2 处调用(build_keymap_picker_params_for_action, keymap_debug_tab);外部调用 2 个(new, from)。
action_count_line448–453 ↗
fn action_count_line(count: usize) -> String
作用:把动作数量变成适合显示的一句话,并处理 1 个和多个时的英文单复数。
数据流:输入是动作数量 → 如果是 1,就输出 1 action.;否则输出类似 5 actions. → 不改动任何外部状态。
调用关系:build_keymap_picker_params_for_action 用它生成各个标签页页头里的统计摘要。它是一个很小的文字格式化帮手。
调用图:被 1 处调用(build_keymap_picker_params_for_action);外部调用 1 个(format!)。
keymap_picker_hint_line455–469 ↗
fn keymap_picker_hint_line() -> Line<'static>
作用:生成快捷键选择器底部的操作提示,告诉用户左右切换分组、回车编辑、星号和减号是什么意思、Esc 关闭。
数据流:它不需要业务输入 → 它取统一强调色,拼出一行带样式的提示文字 → 输出这行提示给选择器底部显示。
调用关系:build_keymap_picker_params_for_action 在创建整个选择器时把它放进页脚。它帮助用户理解这个页面怎么操作。
调用图:调用 1 个内部函数(accent_style);被 1 处调用(build_keymap_picker_params_for_action);外部调用 2 个(from, vec!)。
keymap_debug_hint_line471–479 ↗
fn keymap_debug_hint_line() -> Line<'static>
作用:生成调试标签页专用的底部提示,告诉用户回车开始检查按键,Esc 关闭。
数据流:它不接收输入 → 它用统一强调色拼出调试页的简短提示文字 → 输出一行可显示的提示。
调用关系:快捷键选择器会把它作为 Debug 标签页的专用页脚提示。普通标签页用 keymap_picker_hint_line,调试页用这个更贴合当前任务的提示。
调用图:调用 1 个内部函数(accent_style);外部调用 2 个(from, vec!)。
宠物选择和运行时渲染
这些文件涵盖宠物运行时动画状态、预览数据、选择器构建,以及用于选择和保存宠物的应用级处理。
tui/src/pets/picker.rs源码 ↗
domain_logic · 用户打开 /pets 选择弹窗时活跃;测试只在自动测试阶段活跃
这个文件像是在组装一个“选宠物的菜单”。它先把三类东西合在一起:程序自带的宠物、一个专门用来关闭宠物的选项、用户自己放到配置目录里的宠物。然后它把这些条目排好序,但会把“关闭宠物”固定放在最前面,方便用户快速找到。它还会判断哪个宠物是当前正在用的,哪个应该默认高亮。这里有个细节:如果用户还没配置过宠物,它会默认高亮 Codex,但不会把 Codex 标成“当前正在用”,避免误导。这个文件本身不负责下载或显示预览图片。它只是在用户移动光标或点选时发出事件,好让外层聊天界面去加载预览、保存选择。后半部分是测试,用临时目录假装有自定义宠物,检查菜单内容、排序、搜索词和旧格式头像导入是否正确。
build_pet_picker_params46–136 ↗
fn build_pet_picker_params(
current_pet: Option<&str>,
codex_home: &Path,
preview_state: PetPickerPreviewState,
) -> SelectionViewParams
作用:组装 /pets 弹窗需要的全部参数:标题、列表、搜索、右侧预览区、默认选中项,以及用户点选后要发出的事件。外层界面调用它,就能拿到一个可以直接显示的宠物选择窗口配置。
数据流:进去的是当前宠物编号、Codex 的主目录路径、以及预览区状态。它先找出所有可选宠物,把它们排序,并把“关闭宠物”放到第一项;再逐个变成界面列表项,标出当前正在用的宠物,设置搜索关键词和点击动作;最后输出一个 SelectionViewParams,也就是弹窗完整说明书。过程中它不会直接保存配置,也不会自己加载图片,只会安排好事件:光标变化时请求预览,点选时发送选择或关闭宠物的消息。
调用关系:这是这个文件的主入口。测试函数会直接调用它来验证菜单行为;真实运行时,打开 /pets 弹窗的界面代码也会用它。它把“找宠物”的活交给 available_pet_entries,把底部提示交给 standard_popup_hint_line,把右侧预览区交给 preview_state.renderable(),自己负责把这些零件拼成最终弹窗。
调用图:调用 3 个内部函数(standard_popup_hint_line, available_pet_entries, renderable);被 4 处调用(picker_imports_legacy_avatar_manifests, picker_lists_app_bundled_and_custom_pets, picker_marks_disabled_pet_as_current, picker_preselects_codex_without_marking_it_current_when_no_pet_is_configured);外部调用 3 个(new, default, Fixed)。
available_pet_entries138–156 ↗
fn available_pet_entries(codex_home: &Path) -> Vec<PetPickerEntry>
作用:收集菜单里应该出现的所有宠物条目。它把程序自带宠物、“关闭宠物”这一项、以及用户自定义宠物放到同一个列表里。
数据流:进去的是 Codex 主目录路径。它先从内置宠物清单里复制出名字、编号和说明;再额外加入一个“Disable terminal pets”的关闭选项;最后把 custom_pet_entries 找到的自定义宠物接到列表后面。出来的是一组还没排序的宠物菜单条目。
调用关系:build_pet_picker_params 在组装弹窗前会先叫它准备原料。它自己负责内置宠物和关闭选项,遇到用户自定义宠物时再把工作交给 custom_pet_entries。
调用图:调用 1 个内部函数(custom_pet_entries);被 1 处调用(build_pet_picker_params)。
custom_pet_entries158–194 ↗
fn custom_pet_entries(codex_home: &Path) -> Vec<PetPickerEntry>
作用:从用户的配置目录里寻找自定义宠物,并把能正常读取的宠物变成菜单项。它还兼容旧名字:以前叫 avatar 的目录和文件,也能被当成宠物导入。
数据流:进去的是 Codex 主目录路径。它会分别查看 avatars 目录里的 avatar.json 和 pets 目录里的 pet.json;每看到一个合法清单文件,就从文件夹名推导宠物编号,跳过非法或保留的名字;然后用 Pet::load_with_codex_home 真正读取宠物资料。成功的话,它输出包含显示名、说明、选择编号和旧编号的条目;失败或文件不完整就静静跳过,不让坏文件弄坏整个菜单。
调用关系:available_pet_entries 会调用它来补充用户自己的宠物。它依赖 custom_pet_selector 把普通文件夹名变成内部使用的 custom:xxx 形式,也依赖 Pet::load_with_codex_home 读取宠物配置。这样主弹窗不用关心磁盘目录长什么样。
调用图:调用 2 个内部函数(load_with_codex_home, custom_pet_selector);被 1 处调用(available_pet_entries);外部调用 3 个(new, join, read_dir)。
tests::write_pet200–216 ↗
fn write_pet(dir: &Path, folder_name: &str, display_name: &str)
作用:测试用的小帮手,用来在临时目录里伪造一个新版自定义宠物。这样测试不用依赖用户电脑上真的有宠物文件。
数据流:进去的是临时目录、宠物文件夹名和显示名。它创建 pets/<folder_name> 目录,写入一个 pet.json,再写一张测试用 spritesheet 图片文件。出来没有返回值,但磁盘上的临时目录会多出一个看起来完整的自定义宠物。
调用关系:tests::picker_lists_app_bundled_and_custom_pets 会用它先准备测试数据,然后再调用 build_pet_picker_params 看菜单是否能把这个自定义宠物列出来。它把写测试图片的细节交给 catalog::write_test_spritesheet。
调用图:调用 1 个内部函数(write_test_spritesheet);外部调用 4 个(join, format!, create_dir_all, write)。
tests::write_legacy_avatar218–233 ↗
fn write_legacy_avatar(dir: &Path, folder_name: &str, display_name: &str)
作用:测试用的小帮手,用来伪造旧格式的自定义头像,也就是放在 avatars 目录、使用 avatar.json 的那种。它用来确认旧用户的数据不会被新菜单漏掉。
数据流:进去的是临时目录、头像文件夹名和显示名。它创建 avatars/<folder_name> 目录,写入旧格式的 avatar.json,再写一张测试图片。出来没有返回值,但临时目录里会出现一个旧格式头像数据。
调用关系:tests::picker_imports_legacy_avatar_manifests 会先调用它制造旧数据,再调用 build_pet_picker_params 检查这个旧头像是否被当成自定义宠物列入菜单。它同样把测试图片生成交给 catalog::write_test_spritesheet。
调用图:调用 1 个内部函数(write_test_spritesheet);外部调用 4 个(join, format!, create_dir_all, write)。
tests::picker_lists_app_bundled_and_custom_pets236–270 ↗
fn picker_lists_app_bundled_and_custom_pets()
作用:检查宠物菜单是否同时列出内置宠物和用户自定义宠物,并且顺序、默认选中项、搜索值都符合预期。
数据流:它先创建一个临时 Codex 主目录,再用 write_pet 放入一个叫 Chefito 的自定义宠物。接着调用 build_pet_picker_params,假装当前宠物是 chefito。最后它检查输出菜单的名字顺序,确认关闭选项在最前,自定义宠物出现在列表里,初始选中位置正确,搜索值是 custom:chefito。
调用关系:这是对主流程的综合测试。它直接调用 build_pet_picker_params,间接覆盖 available_pet_entries 和 custom_pet_entries,证明从磁盘自定义宠物到界面列表项这条路是通的。
调用图:调用 1 个内部函数(build_pet_picker_params);外部调用 4 个(assert_eq!, tempdir, write_pet, default)。
tests::picker_preselects_codex_without_marking_it_current_when_no_pet_is_configured273–284 ↗
fn picker_preselects_codex_without_marking_it_current_when_no_pet_is_configured()
作用:检查一个容易混淆的细节:没有配置宠物时,菜单可以默认高亮 Codex,但不能把它标成当前正在使用。
数据流:它创建一个空的临时 Codex 主目录,然后用 current_pet 为 None 调用 build_pet_picker_params。结果出来后,它检查初始高亮项是 Codex,同时确认 Codex 的 is_current 是 false。也就是说,界面给用户一个合理起点,但不假装用户已经选过它。
调用关系:它专门验证 build_pet_picker_params 里默认选择和当前状态的区别。这个测试保护的是用户体验:默认高亮只是方便操作,不能等同于已经启用。
调用图:调用 1 个内部函数(build_pet_picker_params);外部调用 4 个(assert!, assert_eq!, tempdir, default)。
tests::picker_marks_disabled_pet_as_current287–303 ↗
fn picker_marks_disabled_pet_as_current()
作用:检查当用户当前配置是“关闭宠物”时,菜单第一项会正确显示为当前项,并带有方便搜索关闭功能的关键词。
数据流:它创建临时目录,然后把当前宠物传成 DISABLED_PET_ID 来调用 build_pet_picker_params。返回结果里,它检查第一项就是“Disable terminal pets”,没有说明文字,被标成当前项,而且搜索词包含 disable、hidden、off、none 等方便用户查找的词。
调用关系:它验证 build_pet_picker_params 对特殊关闭项的处理。这个关闭项不是普通宠物,所以点击后会发送 PetDisabled 事件;测试确保它在列表位置和状态标记上也被特殊对待。
调用图:调用 1 个内部函数(build_pet_picker_params);外部调用 4 个(assert!, assert_eq!, tempdir, default)。
tests::picker_imports_legacy_avatar_manifests306–323 ↗
fn picker_imports_legacy_avatar_manifests()
作用:检查旧版 avatars/avatar.json 格式仍然能出现在新宠物菜单里。这样老用户升级后,原来的自定义头像不会突然消失。
数据流:它先创建临时目录,再用 write_legacy_avatar 写入一个旧格式头像。然后调用 build_pet_picker_params,当前宠物传入 custom:legacy。最后它在输出菜单中找到名为 Legacy 的条目,确认它被标成当前项,并且搜索值是新的 custom:legacy 形式。
调用关系:它主要覆盖 custom_pet_entries 的兼容逻辑,但通过 build_pet_picker_params 从外部验证整个菜单结果。这个测试说明旧编号和新选择编号之间的桥接没有断。
调用图:调用 1 个内部函数(build_pet_picker_params);外部调用 5 个(assert!, assert_eq!, tempdir, write_legacy_avatar, default)。
tui/src/app/pets.rs源码 ↗
这个文件像宠物功能的“现场调度员”。用户在终端界面里选宠物、预览宠物,或者程序准备退出时,都会走到这里。它不亲自画图,也不亲自改配置文件,而是把这些事交给更合适的部件:让 Tui 去清理或重画图片,让 chat_widget 更新弹窗和提示,让配置编辑器把选择保存下来。这里一个重要设计是区分两类错误:终端错误,也就是显示环境本身出问题,通常要往外报;资源错误,也就是宠物图片或素材坏了,多数情况下只记录警告、清掉画面、禁用本次宠物,避免影响主程序。选择宠物时,它还会把加载工作丢到后台线程,防止界面卡住;等加载结果回来,再更新界面并请求重画。
App::disable_ambient_pet_before_shutdown6–20 ↗
fn disable_ambient_pet_before_shutdown(&mut self, tui: &mut tui::Tui) -> Result<()>
作用:在程序退出前关掉环境宠物,并尽量把终端上的宠物图片擦掉。这样退出时不会留下半张图片或奇怪的显示残影。
数据流:进去的是当前 App 状态和 Tui 终端界面对象 → 它先告诉聊天组件本次会话不要再显示宠物,然后调用终端界面去清掉宠物图片 → 如果清理时是终端本身出错,就把错误返回;如果只是宠物素材相关错误,就记一条警告后继续退出。
调用关系:它通常在关机或退出流程前被调用。它把真正清屏的活交给 tui.clear_ambient_pet_image,遇到不严重的素材问题时只通过 warn! 记录,避免退出流程被小问题卡住。
调用图:外部调用 2 个(clear_ambient_pet_image, warn!)。
App::handle_ambient_pet_image_render_error22–49 ↗
fn handle_ambient_pet_image_render_error(
&mut self,
tui: &mut tui::Tui,
err: crate::pets::PetImageRenderError,
) -> Result<()>
作用:处理常驻宠物图片画不出来的情况。它的目标是保护主界面:终端坏了就报错,素材坏了就禁用宠物并清理画面。
数据流:进去的是 App、Tui,以及一次宠物图片渲染错误 → 它先判断错误类型:如果是终端错误,就转换成上层能处理的错误并返回;如果是素材错误,就记录警告,禁用本次会话的宠物,再尝试清掉已经显示的宠物图片 → 最后返回成功,除非清理时又遇到终端错误。
调用关系:它位于宠物图片绘制失败后的补救流程里。它调用 tui.clear_ambient_pet_image 做现场清理,用 warn! 记录素材问题,并在终端错误时通过 into 把错误交给上层流程处理。
调用图:外部调用 3 个(clear_ambient_pet_image, warn!, into)。
App::handle_pet_picker_preview_image_render_error51–76 ↗
fn handle_pet_picker_preview_image_render_error(
&mut self,
tui: &mut tui::Tui,
err: crate::pets::PetImageRenderError,
) -> Result<()>
作用:处理宠物选择器里的预览图画不出来的情况。它会告诉界面“预览失败了”,同时尽量把预览区域清干净。
数据流:进去的是 App、Tui,以及预览图渲染错误 → 如果是终端错误,就直接返回错误;如果是素材错误,就记录警告,把错误文字交给聊天组件显示为预览失败,再请求 Tui 用空请求重画预览区域,相当于清空预览图 → 如果清空时只是素材问题就继续,如果是终端问题就返回错误。
调用关系:它在宠物选择弹窗的预览图片失败时被用到。它把用户可见的失败状态交给 chat_widget,把清理图片的工作交给 tui.draw_pet_picker_preview_image,并用 to_string 把底层错误变成能显示给用户看的文字。
调用图:外部调用 4 个(draw_pet_picker_preview_image, warn!, into, to_string)。
App::handle_pet_selected78–103 ↗
fn handle_pet_selected(&mut self, tui: &mut tui::Tui, pet_id: String)
作用:用户选中某个宠物后,这个函数启动加载流程。它先让界面显示“正在加载”,再把耗时的准备和读取宠物工作放到后台,避免卡住终端界面。
数据流:进去的是 App、Tui 和用户选择的 pet_id → 它在聊天组件里打开加载弹窗,要求界面重画;接着复制配置目录、动画开关、事件发送器等必要信息 → 然后用后台阻塞任务去确保内置宠物包存在,并加载 AmbientPet → 最后把成功或失败结果通过 AppEvent::PetSelectionLoaded 发回主流程。
调用关系:它是“用户点选宠物”后的第一站。它通过 tui.frame_requester().schedule_frame 让界面立刻刷新,通过 spawn_blocking 把重活放到后台,后台完成后用 app_event_tx 把结果送回,后续通常会由 App::handle_pet_selection_loaded 接着处理。
调用图:外部调用 3 个(frame_requester, drop, spawn_blocking)。
App::handle_pet_disabled105–121 ↗
async fn handle_pet_disabled(&mut self, tui: &mut tui::Tui)
作用:处理用户选择关闭宠物的操作。它会把“禁用宠物”写进配置,这样不只是当前界面关掉,下次启动也会记住。
数据流:进去的是 App 和 Tui → 它先创建一条配置修改,内容是把 TUI 宠物设为禁用 ID → 用 ConfigEditsBuilder 把修改写到配置里 → 如果成功,就同步当前界面状态并请求重画;如果失败,就在聊天界面显示一条错误消息。
调用关系:它在用户明确关闭宠物时运行。它调用 tui_pet_edit 生成配置改动,用 ConfigEditsBuilder::new 开始保存流程,成功后通过 frame_requester 触发刷新,失败时用 format! 拼出用户能看到的错误提示。
调用图:调用 2 个内部函数(new, tui_pet_edit);外部调用 2 个(frame_requester, format!)。
App::handle_pet_preview_loaded123–132 ↗
fn handle_pet_preview_loaded(
&mut self,
tui: &mut tui::Tui,
request_id: u64,
result: Result<crate::pets::AmbientPet, String>,
)
作用:处理宠物预览加载完成的结果。无论成功还是失败,它都会把结果交给宠物选择界面,并要求终端重画。
数据流:进去的是 App、Tui、请求编号 request_id,以及加载出的 AmbientPet 或错误文字 → 它把这些交给聊天组件,让组件按请求编号结束对应的预览加载状态 → 然后请求下一帧刷新,让用户看到预览图或失败提示。
调用关系:它通常接在后台预览加载任务之后。它不自己解释结果,而是交给 chat_widget.finish_pet_picker_preview_load,再通过 tui.frame_requester().schedule_frame 推动界面更新。
调用图:外部调用 1 个(frame_requester)。
App::handle_pet_selection_loaded134–173 ↗
async fn handle_pet_selection_loaded(
&mut self,
tui: &mut tui::Tui,
request_id: u64,
pet_id: String,
result: Result<Option<crate::pets::AmbientPet>, String>,
作用:处理“用户选择的宠物已经加载完”这件事。它会关闭加载弹窗,成功时保存选择并更新界面,失败时显示错误。
数据流:进去的是 App、Tui、加载弹窗的 request_id、pet_id,以及加载结果 → 它先用 request_id 确认这个结果还对应当前弹窗;如果不是,就什么也不改,继续运行 → 如果加载成功,就生成配置修改,把选择的宠物写入配置,成功后更新内存里的 config.tui_pet 和聊天组件里的宠物对象;如果保存失败或加载失败,就显示错误消息 → 最后请求界面重画,并返回继续运行。
调用关系:它通常接收 App::handle_pet_selected 后台任务发回来的结果。它调用 tui_pet_edit 和 ConfigEditsBuilder::new 来保存配置,用 chat_widget 更新弹窗和宠物状态,并通过 frame_requester 让用户看到最终变化。
调用图:调用 2 个内部函数(new, tui_pet_edit);外部调用 2 个(frame_requester, format!)。
App::handle_configured_pet_loaded175–196 ↗
fn handle_configured_pet_loaded(
&mut self,
tui: &mut tui::Tui,
pet_id: String,
result: Result<Option<crate::pets::AmbientPet>, String>,
)
作用:处理启动或配置读取后,已配置宠物的加载结果。它会确认结果是不是当前配置想要的宠物,避免旧结果覆盖新选择。
数据流:进去的是 App、Tui、pet_id 和加载结果 → 它先检查当前配置里的宠物 ID 是否仍然等于这个 pet_id;如果用户已经换了选择,就直接丢弃这个过期结果 → 如果匹配且加载成功,就把宠物对象交给聊天组件并请求重画;如果加载失败,就在界面里显示一条警告。
调用关系:它用于处理“按配置自动加载宠物”的异步结果。它的关键位置是防止后台慢结果污染当前状态;成功时调用 chat_widget.set_tui_pet_loaded 并用 frame_requester 刷新,失败时用 format! 生成警告文字。
调用图:外部调用 2 个(frame_requester, format!)。
tui/src/pets/ambient.rs源码 ↗
这个文件处理的是“终端角落里的宠物怎么出现”。宠物图片本身来自提前拆好的帧图,像动画片的一张张画;这里决定现在该用哪一张、放在屏幕哪里、什么时候该刷新下一张。它还会根据通知状态切换动作,比如运行中用 running,等待用户用 waiting。如果终端不支持图片协议(也就是终端不能直接显示图片),它会干脆不画,避免输出乱码。画图时它会先算宠物占多少行多少列,再把它放到输入框上方,并留出一点空隙,防止盖住底部操作区。它不直接把图片写进终端,而是生成一个 AmbientPetDraw 请求,交给后面的渲染层去真正显示。
PetNotificationKind::animation_name55–62 ↗
fn animation_name(self) -> &'static str
作用:把宠物通知类型转换成动画名字。比如“运行中”会对应到名叫 running 的动画。
数据流:进去的是一个通知种类 → 它用固定对应表查出动画名称 → 出来的是一段短字符串,后面会拿这个名字去宠物动画表里找动画。
调用关系:它是状态到动画之间的翻译员;当 AmbientPet::current_animation 要决定当前播放哪套动作时,会需要这种对应关系。
PetNotificationKind::label64–71 ↗
fn label(self) -> &'static str
作用:给通知种类取一个界面上能给人看的短标签。比如 Failed 会显示成 Blocked。
数据流:进去的是通知种类 → 它选择一个固定英文标签 → 出来的是给界面或高度判断使用的文字。
调用关系:它和 notification_height 配合使用:如果通知正文刚好等于这个短标签,就只占一行;否则可能需要两行。测试也会检查这些词和主应用的说法一致。
PetNotificationKind::fallback_body73–80 ↗
fn fallback_body(self) -> &'static str
作用:当调用方没有给通知正文时,给它补一个默认说明。这样宠物不会出现空消息。
数据流:进去的是通知种类 → 它按种类挑一句默认正文 → 出来的是可显示的文字,比如 Thinking 或 Ready。
调用关系:PetNotification::new 会用它兜底,保证 set_notification 即使只传状态、不传文字,也能得到完整通知。
PetNotificationKind::lifetime82–89 ↗
fn lifetime(self) -> Duration
作用:规定每种通知最多能显示多久。比如“运行中”很短,“等待输入”和“待审阅”可以留很久。
数据流:进去的是通知种类 → 它返回一个持续时间 → 后面用这个时间判断通知是否过期。
调用关系:PetNotification::is_expired 会调用它;这让不同状态有不同“保质期”,避免旧状态一直挂在屏幕上误导用户。
调用图:被 1 处调用(is_expired)。
PetNotification::new100–106 ↗
fn new(kind: PetNotificationKind, body: Option<String>) -> Self
作用:新建一条宠物通知,并记下它是什么时候创建的。有人更新宠物状态时会用它。
数据流:进去的是通知种类和可选正文 → 如果正文没有给,就用 fallback_body 补上;同时读取当前时间 → 出来是一条带状态、文字、更新时间的 PetNotification。
调用关系:AmbientPet::set_notification 调用它,把外部传来的状态变成宠物内部能保存和过期判断的通知对象。
调用图:被 1 处调用(set_notification);外部调用 1 个(now)。
PetNotification::is_expired108–110 ↗
fn is_expired(&self, now: Instant) -> bool
作用:判断这条通知是不是已经显示太久了。过期后,宠物就不应该继续按这个状态显示。
数据流:进去的是当前时间 → 它用当前时间减去通知更新时间,再和该通知种类的 lifetime 比较 → 出来是真或假,表示是否过期。
调用关系:AmbientPet::visible_notification 会靠它过滤旧通知;它内部调用 PetNotificationKind::lifetime 来知道不同状态该保留多久。
调用图:调用 1 个内部函数(lifetime);外部调用 1 个(saturating_duration_since)。
AmbientPet::load146–176 ↗
fn load(
selected_pet: Option<&str>,
codex_home: &std::path::Path,
frame_requester: FrameRequester,
animations_enabled: bool,
) -> Result<Self>
作用:加载当前选中的宠物,并准备好它要用的图片帧缓存。没有这一步,后面就不知道宠物长什么样、有哪些动画帧。
数据流:进去的是宠物 id、Codex 主目录、刷新请求器和是否允许动画 → 它加载宠物配置,拼出缓存目录,把精灵图拆成 PNG 帧,检测图片显示支持情况,并记录开始时间 → 出来的是一个可以参与绘制的 AmbientPet。
调用关系:它由上层的 load_ambient_pet 调用,是 ambient 宠物进入运行状态的入口;它把宠物模型加载、帧准备、图片协议支持这些零件组装到同一个对象里。
调用图:调用 3 个内部函数(default_image_support, prepare_png_frames, load_with_codex_home);被 1 处调用(load_ambient_pet);外部调用 2 个(now, join)。
AmbientPet::set_notification178–181 ↗
fn set_notification(&mut self, kind: PetNotificationKind, body: Option<String>)
作用:告诉宠物当前发生了什么事,比如正在运行、等用户输入或失败了。它还会从这一刻重新开始播放对应动画。
数据流:进去的是通知种类和可选正文 → 它创建 PetNotification,并把动画开始时间重置为当前时间 → AmbientPet 内部的 notification 和 animation_started_at 被更新。
调用关系:上层状态变化时会调用它;它把变化交给 PetNotification::new 封装,之后 current_animation 和 current_frame_path 会根据新状态选动画帧。
AmbientPet::image_enabled183–185 ↗
fn image_enabled(&self) -> bool
作用:告诉外部:当前终端能不能显示宠物图片。这样外部可以决定要不要为宠物留位置或发图像请求。
数据流:它读取 AmbientPet 里保存的图片支持信息 → 检查有没有可用协议 → 出来是真或假。
调用关系:它依赖 PetImageSupport 的 protocol 判断;这是一个轻量查询函数,供界面层在绘制前做选择。
调用图:调用 1 个内部函数(protocol)。
AmbientPet::image_columns187–189 ↗
fn image_columns(&self) -> u16
作用:告诉外部宠物图片大概要占多少终端列。外部布局可以用它避免内容和宠物挤在一起。
数据流:它读取宠物帧宽高 → 调用 image_size 算出显示尺寸 → 出来的是列数。
调用关系:它是 AmbientPet::image_size 的简化出口;当调用方只关心横向占用时,不需要拿完整尺寸。
调用图:调用 1 个内部函数(image_size)。
AmbientPet::set_image_support_for_tests192–194 ↗
fn set_image_support_for_tests(&mut self, support: PetImageSupport)
作用:测试专用:强行设置终端图片支持情况。这样测试不用真的依赖某个终端环境。
数据流:进去的是一份 PetImageSupport → 它直接替换 AmbientPet 内部的 support → 后续 image_enabled、draw_request 等都会按这份假环境运行。
调用关系:只在测试编译时存在;它让测试可以模拟支持 Kitty、Sixel 或完全不支持图片的情况。
AmbientPet::schedule_next_frame196–200 ↗
fn schedule_next_frame(&self)
作用:安排下一次动画刷新。动画不是一次画完的,它需要在每帧持续时间结束后再重画。
数据流:它先询问 next_frame_delay 下一帧还要等多久 → 如果有时间,就交给 FrameRequester 安排未来刷新 → 它本身不返回绘制结果,只触发调度。
调用关系:主循环画完一帧后可以调用它;它把“什么时候该再画”这件事交给 frame_requester 的 schedule_frame_in。
调用图:调用 2 个内部函数(next_frame_delay, schedule_frame_in)。
AmbientPet::next_frame_delay202–212 ↗
fn next_frame_delay(&self) -> Option<Duration>
作用:算出距离下一张动画帧还有多久。如果终端不能显示图片,或用户关闭动画,就不需要安排下一帧。
数据流:它读取图片协议支持、动画开关、当前动画和已经播放的时间 → 找到当前帧,并拿到这帧剩余时间 → 出来是一个可选的等待时间。
调用关系:AmbientPet::schedule_next_frame 调用它;它会继续调用 current_animation 和 current_animation_frame,把状态选择和逐帧计时连起来。
调用图:调用 3 个内部函数(current_animation, current_animation_frame, protocol);被 1 处调用(schedule_next_frame);外部调用 1 个(elapsed)。
AmbientPet::draw_request221–249 ↗
fn draw_request(
&self,
area: Rect,
composer_bottom_y: u16,
) -> Option<AmbientPetDraw>
作用:为主界面生成一次宠物绘制请求。它会确保宠物贴在输入框上方,不盖住保留区域。
数据流:进去的是可绘制区域和输入框底部位置 → 它检查终端图片协议、计算图片大小、通知高度和间距,再选当前帧路径 → 如果空间足够,出来一个 AmbientPetDraw;如果不够或不能显示图片,就返回空。
调用关系:主界面每次绘制时会用它拿到“该把哪张图放在哪里”;它调用 current_frame_path、image_size、visible_notification 和 composer_gap_rows,把动画、尺寸和布局合成一个请求。
调用图:调用 5 个内部函数(current_frame_path, image_size, visible_notification, composer_gap_rows, protocol);外部调用 2 个(now, clone)。
AmbientPet::preview_draw_request256–275 ↗
fn preview_draw_request(&self, area: Rect) -> Option<AmbientPetDraw>
作用:为 /pets 选择面板生成居中的宠物预览图。它故意用 idle 的第一帧,让用户浏览时画面稳定不乱跳。
数据流:进去的是预览区域 → 它检查图片协议和区域大小,计算居中坐标,并取第一张 idle 帧 → 空间够就返回 AmbientPetDraw,不够就返回空。
调用关系:宠物选择器侧边栏会用它;它不像 draw_request 那样跟随实时通知,而是调用 first_idle_frame_path 保持预览固定。
调用图:调用 3 个内部函数(first_idle_frame_path, image_size, protocol);外部调用 1 个(clone)。
AmbientPet::visible_notification277–281 ↗
fn visible_notification(&self, now: Instant) -> Option<&PetNotification>
作用:取出当前还没过期的通知。过期通知会被当作不存在。
数据流:进去的是当前时间 → 它查看内部 notification,并用 is_expired 判断是否仍有效 → 出来是通知引用,或没有通知。
调用关系:AmbientPet::draw_request 用它计算通知占用高度;AmbientPet::current_animation 用它决定播放 idle 还是状态动画。
调用图:被 2 处调用(current_animation, draw_request)。
AmbientPet::current_animation283–301 ↗
fn current_animation(&self) -> Option<&Animation>
作用:决定宠物现在应该播放哪套动画。优先看有效通知;没有通知就播放 idle。
数据流:它读取当前通知、宠物动画表和动画已播放时间 → 按通知种类找动画,找不到就退回 idle;如果一次性动画播完且有 fallback,就切到 fallback 动画 → 出来是一套 Animation。
调用关系:AmbientPet::current_frame_path 和 AmbientPet::next_frame_delay 都依赖它;它是“状态”到“动画”之间的核心选择器。
调用图:调用 1 个内部函数(visible_notification);被 2 处调用(current_frame_path, next_frame_delay);外部调用 2 个(elapsed, now)。
AmbientPet::current_frame_path303–316 ↗
fn current_frame_path(&self) -> Option<PathBuf>
作用:找出当前这一刻应该显示哪一个图片文件。它把动画进度转换成真实 PNG 帧路径。
数据流:它先拿 current_animation → 如果动画开启,就按 elapsed 时间找当前帧;如果关闭,就固定用动画第一帧 → 再把 sprite_index 转成缓存里的文件路径。
调用关系:AmbientPet::draw_request 调用它来填 AmbientPetDraw.frame;它把 current_animation 和 frame_path_for_sprite_index 串起来。
调用图:调用 2 个内部函数(current_animation, frame_path_for_sprite_index);被 1 处调用(draw_request)。
AmbientPet::first_idle_frame_path318–326 ↗
fn first_idle_frame_path(&self) -> Option<PathBuf>
作用:取 idle 动画的第一帧图片。主要用于稳定预览,不用于实时动画播放。
数据流:它读取宠物动画表里的 idle 动画 → 找第一帧的 sprite_index,找不到就用 0 → 再转成实际帧文件路径。
调用关系:AmbientPet::preview_draw_request 调用它;它和 current_frame_path 的区别是完全不看通知和时间。
调用图:调用 1 个内部函数(frame_path_for_sprite_index);被 1 处调用(preview_draw_request)。
AmbientPet::frame_path_for_sprite_index328–332 ↗
fn frame_path_for_sprite_index(&self, sprite_index: usize) -> Option<PathBuf>
作用:把精灵图里的帧编号转换成已经拆出来的 PNG 文件路径。编号越界时会夹到最后一张,避免崩掉。
数据流:进去的是 sprite_index → 它在 frames 列表里取对应文件;如果编号太大,就取最后一个可用位置 → 出来是路径副本,或没有路径。
调用关系:AmbientPet::current_frame_path 和 AmbientPet::first_idle_frame_path 都调用它;它是动画帧编号到磁盘缓存文件之间的小桥。
调用图:被 2 处调用(current_frame_path, first_idle_frame_path)。
AmbientPet::image_size334–345 ↗
fn image_size(&self) -> ImageSize
作用:计算宠物在终端里应该占多少列、多少行,以及图片高度像素。这样布局和图片协议能说同一种尺寸。
数据流:它读取宠物原始帧宽高和固定目标高度 → 按终端行高估算行数,再按宽高比例估算列数 → 出来是 ImageSize。
调用关系:draw_request、preview_draw_request 和 image_columns 都会调用它;它保证主界面和预览使用同一套尺寸算法。
调用图:被 3 处调用(draw_request, image_columns, preview_draw_request);外部调用 1 个(from)。
composer_gap_rows348–351 ↗
fn composer_gap_rows() -> u16
作用:把宠物和输入框之间的像素间距换算成终端行数。简单说,就是留一条安全缝。
数据流:它读取固定的像素间距和终端行高 → 做四舍五入,并保证至少一行 → 出来的是需要空出来的行数。
调用关系:AmbientPet::draw_request 调用它;这个间距让宠物不会紧贴或压住 composer 输入区。
调用图:被 1 处调用(draw_request);外部调用 1 个(from)。
default_image_support359–361 ↗
fn default_image_support() -> PetImageSupport
作用:给 AmbientPet 一个默认的图片显示能力判断。正常运行时会自动选择可用协议;测试版本会默认说不支持,避免依赖真实终端。
数据流:在对应编译环境里,它生成一份 PetImageSupport → load 把这份结果存进 AmbientPet → 后续绘制都以它为准。
调用关系:AmbientPet::load 调用它;它是宠物图片能不能显示、用什么协议显示的默认来源。
调用图:被 1 处调用(load);外部调用 1 个(Unsupported)。
current_animation_frame376–412 ↗
fn current_animation_frame(animation: &Animation, elapsed: Duration) -> Option<AnimationFrameTick>
作用:根据动画已经播放了多久,算出现在应该是哪一帧,以及多久后需要换下一帧。
数据流:进去的是一套 Animation 和 elapsed 时间 → 它处理单帧、循环动画、非循环动画结束等情况 → 出来的是 AnimationFrameTick,里面有帧编号和下一次刷新延迟。
调用关系:AmbientPet::next_frame_delay 用它安排刷新;current_frame_path 也通过同样思路选择帧。它会把具体逐帧查找交给 frame_at_elapsed。
调用图:调用 2 个内部函数(frame_at_elapsed, total_duration);被 1 处调用(next_frame_delay);外部调用 1 个(as_nanos)。
frame_at_elapsed414–431 ↗
fn frame_at_elapsed(animation: &Animation, elapsed_nanos: u128) -> Option<AnimationFrameTick>
作用:在一串动画帧里,根据经过的时间找到落在哪一帧。可以把它想成拿秒表从第一张图一路数过去。
数据流:进去的是动画和纳秒数 → 它逐帧扣掉每帧时长,直到找到当前时间所在的帧 → 出来的是帧编号和这一帧还剩多久。
调用关系:current_animation_frame 在处理普通播放和循环播放时都会调用它;它再调用 nanos_to_duration 把纳秒差值变回 Duration。
调用图:调用 1 个内部函数(nanos_to_duration);被 1 处调用(current_animation_frame)。
nanos_to_duration433–435 ↗
fn nanos_to_duration(nanos: u128) -> Duration
作用:把纳秒数字安全地转换成 Duration 时间对象。它会防止数字太大超过系统能表示的范围。
数据流:进去的是 u128 纳秒数 → 它把数值限制到 u64 最大值以内 → 出来的是 Duration。
调用关系:frame_at_elapsed 用它生成“还要等多久换帧”的时间;这是动画计时里的安全转换小工具。
调用图:被 1 处调用(frame_at_elapsed);外部调用 2 个(from_nanos, from)。
notification_height437–443 ↗
fn notification_height(notification: &PetNotification) -> u16
作用:估算通知文字在宠物旁边或上方需要占几行。短标签占一行,带额外正文时占两行。
数据流:进去的是一条 PetNotification → 它比较正文是否等于该状态的短标签 → 出来是 1 或 2 行高度。
调用关系:AmbientPet::draw_request 用它计算总共需要给宠物和通知留多少竖向空间,避免画面压到输入框。
test_ambient_pet446–473 ↗
fn test_ambient_pet(
frame_requester: FrameRequester,
animations_enabled: bool,
) -> AmbientPet
作用:测试专用:快速造一个假的 AmbientPet。这样测试不用读取真实宠物文件,也不用真的拆图片。
数据流:进去的是测试用 FrameRequester 和动画开关 → 它手工填好宠物信息、两张帧路径、图片协议和动画开始时间 → 出来是可直接测试的 AmbientPet。
调用关系:reduced_motion_uses_stable_first_frame_and_schedules_no_follow_up 测试会调用它;它还调用 test_animation 准备最小动画数据。
调用图:调用 1 个内部函数(test_animation);被 1 处调用(reduced_motion_uses_stable_first_frame_and_schedules_no_follow_up);外部调用 8 个(from_millis, from, now, from, new, new, Supported, vec!)。
test_animation476–491 ↗
fn test_animation() -> Animation
作用:测试专用:创建一个很小的两帧循环动画。它让动画时间计算可以被简单验证。
数据流:它不需要外部输入 → 生成两帧,每帧 10 毫秒,并设置从第 0 帧开始循环 → 出来是一份 Animation。
调用关系:test_ambient_pet 和 animation_frame_uses_per_frame_duration 测试都会用它;它提供稳定、可预期的动画样本。
调用图:被 2 处调用(test_ambient_pet, animation_frame_uses_per_frame_duration);外部调用 1 个(vec!)。
tests::notification_labels_match_codex_app_vocabulary498–503 ↗
fn notification_labels_match_codex_app_vocabulary()
作用:测试宠物通知标签是否和 Codex 应用里的用词一致。这样界面不会一处叫 Ready,另一处叫别的名字。
数据流:它读取各个 PetNotificationKind 的 label → 和预期字符串逐个比较 → 如果不一致,测试失败。
调用关系:这是对 PetNotificationKind::label 的保护网;以后有人改文案时,会立刻看到是否破坏了统一用词。
调用图:外部调用 1 个(assert_eq!)。
tests::animation_frame_uses_per_frame_duration506–516 ↗
fn animation_frame_uses_per_frame_duration()
作用:测试动画选帧是否真的尊重每一帧自己的时长。它确认 15 毫秒时应该落在第二帧,并且 5 毫秒后再刷新。
数据流:它先用 test_animation 建一个两帧动画 → 把 elapsed 设为 15 毫秒并调用 current_animation_frame → 把结果和期望的帧编号、延迟比较。
调用关系:这是 current_animation_frame 和 frame_at_elapsed 的行为测试;它防止动画计时退化成简单平均或固定帧率。
调用图:调用 1 个内部函数(test_animation);外部调用 1 个(assert_eq!)。
tests::reduced_motion_uses_stable_first_frame_and_schedules_no_follow_up519–527 ↗
fn reduced_motion_uses_stable_first_frame_and_schedules_no_follow_up()
作用:测试关闭动画时,宠物会固定显示第一帧,并且不再安排下一次动画刷新。这对不喜欢动态效果的用户很重要。
数据流:它创建一个 animations_enabled 为 false 的测试宠物 → 检查 current_frame_path 是否是 frame-0.png,再检查 next_frame_delay 是否为空 → 不符合就测试失败。
调用关系:它调用 test_ambient_pet 搭测试对象;它覆盖 current_frame_path 和 next_frame_delay 在“减少动态效果”模式下的配合行为。
调用图:调用 2 个内部函数(test_ambient_pet, test_dummy);外部调用 1 个(assert_eq!)。
tui/src/pets/preview.rs源码 ↗
宠物预览不是一个完全静态的文字框:用户选中不同宠物时,图片可能还在异步加载,也可能失败,或者宠物功能被关掉。这个文件把这些状态集中放进 PetPickerPreviewState。它里面用 Arc<Mutex> 保存数据:Arc 可以让多处共享同一份状态,Mutex 像一把锁,防止两边同时改。界面真正要画东西时,会拿到 PetPickerPreviewRenderable,根据当前状态显示“Loading preview...”之类的提示。特别的是,状态为 Ready 时这里不画文字,因为真正的图片可能由外面的渲染流程画到同一块区域;所以它只记住 last_area,方便外部知道图片该放哪里。
PetPickerPreviewState::renderable33–37 ↗
fn renderable(&self) -> PetPickerPreviewRenderable
作用:把预览状态包装成一个可以被界面系统绘制的对象。这样弹窗只需要拿这个对象去画,不必知道宠物图片加载的细节。
数据流:进去的是当前的 PetPickerPreviewState → 它复制一份指向同一内部状态的共享引用,而不是复制整份数据 → 出来一个 PetPickerPreviewRenderable,之后它和控制器看到的是同一份状态。
调用关系:构建宠物选择器参数时会调用它。它把状态交给界面渲染层使用,后续 render 会读取这份共享状态来决定显示什么。
调用图:被 1 处调用(build_pet_picker_params);外部调用 1 个(clone)。
PetPickerPreviewState::set_loading39–43 ↗
fn set_loading(&self)
作用:把预览栏切换成“正在加载”的状态。用户刚选中一个宠物、预览还没准备好时会用到它。
数据流:进去的是当前状态对象 → 它通过 update 拿到内部状态并把状态改成 Loading → 之后界面再绘制时会显示加载提示。
调用关系:它不直接画界面,只负责改状态。真正的加锁和修改由 update 完成,之后 PetPickerPreviewRenderable::render 会读到这个变化。
调用图:调用 1 个内部函数(update)。
PetPickerPreviewState::set_disabled45–49 ↗
fn set_disabled(&self)
作用:把预览栏切换成“宠物功能已关闭”的状态。这样用户不会误以为是加载失败或界面坏了。
数据流:进去的是当前状态对象 → 它通过 update 把内部状态改成 Disabled → 出来的效果是下一次绘制时显示“Terminal pets disabled”和说明文字。
调用关系:它把具体改状态的活交给 update。之后渲染函数根据这个状态画出禁用提示。
调用图:调用 1 个内部函数(update)。
PetPickerPreviewState::set_ready51–55 ↗
fn set_ready(&self)
作用:把预览栏标记为“预览已准备好”。这通常表示文字提示可以消失,外部图片渲染可以接手。
数据流:进去的是当前状态对象 → 它通过 update 把内部状态改成 Ready → 下一次普通文字渲染会直接退出,不再画加载或错误文字。
调用关系:它由预览准备完成后的流程使用。它只更新共享状态,真正看到变化的是 PetPickerPreviewRenderable::render。
调用图:调用 1 个内部函数(update)。
PetPickerPreviewState::set_error57–61 ↗
fn set_error(&self, message: String)
作用:把预览栏切换成“出错”状态,并保存一段给用户看的错误消息。这样失败时界面能说明原因,而不是空白一片。
数据流:进去的是当前状态对象和一段错误文字 → 它通过 update 把状态改成 Error,并把这段文字存进去 → 下一次绘制时会显示“Preview unavailable”和错误详情。
调用关系:加载预览失败的流程会用它记录失败结果。它依赖 update 安全修改状态,渲染函数随后读取并显示这条消息。
调用图:调用 1 个内部函数(update)。
PetPickerPreviewState::clear63–68 ↗
fn clear(&self)
作用:清空预览栏状态,让它回到隐藏状态,并忘掉上次的绘制区域。比如关闭弹窗或重置选择时会用到。
数据流:进去的是当前状态对象 → 它通过 update 把状态改成 Hidden,同时把 last_area 清成空 → 之后不会再显示提示,外部也拿不到旧的预览位置。
调用关系:它是重置按钮一样的函数。具体修改由 update 完成,后续 render 看到 Hidden 就什么也不画。
调用图:调用 1 个内部函数(update)。
PetPickerPreviewState::area70–72 ↗
fn area(&self) -> Option<Rect>
作用:取出预览栏最近一次被安排在屏幕上的位置。外部图片渲染需要知道这块区域,才能把宠物图画到正确地方。
数据流:进去的是当前状态对象 → 它尝试给内部状态加锁并读取 last_area → 出来的是一个可能存在的矩形区域;如果没画过或加锁失败,就返回空。
调用关系:它通常给外部的图片绘制流程使用。last_area 是由 PetPickerPreviewRenderable::render 在每次绘制时更新的。
PetPickerPreviewState::update74–78 ↗
fn update(&self, f: impl FnOnce(&mut PetPickerPreviewInner))
作用:这是所有状态修改共用的小工具,负责先安全拿到锁,再执行具体修改。它让外部函数不用重复写加锁代码。
数据流:进去的是一个修改内部状态的小操作 → 它尝试锁住共享状态,成功后把内部数据交给这个操作去改 → 它本身不返回数据,只改变保存的状态;如果锁失败,就安静地不做事。
调用关系:set_loading、set_disabled、set_ready、set_error 和 clear 都通过它改状态。它是这些状态按钮背后的统一入口。
调用图:被 5 处调用(clear, set_disabled, set_error, set_loading, set_ready)。
PetPickerPreviewRenderable::render104–133 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:根据当前预览状态,在终端预览栏里画出对应提示文字。它也会记住这次绘制区域,方便外部把图片画到同一个位置。
数据流:进去的是屏幕区域 area 和终端缓冲区 buf,缓冲区可以理解成“待画的画布” → 它先锁住共享状态,记录 last_area,再根据状态决定是否显示加载、禁用或错误文字 → 如果需要显示文字,就把文字垂直居中后写进缓冲区;如果是隐藏或已准备好,就不画文字。
调用关系:界面系统需要绘制预览侧栏时会调用它。它会把计算文字位置的工作交给 centered_text_area,再用终端 UI 组件把文字真正写到缓冲区里。
调用图:调用 1 个内部函数(centered_text_area);外部调用 3 个(from, new, vec!)。
PetPickerPreviewRenderable::desired_height135–137 ↗
fn desired_height(&self, _width: u16) -> u16
作用:告诉界面布局系统,这个预览组件希望有多高。这里固定说需要 4 行高度。
数据流:进去的是可用宽度,但这个函数不需要用它 → 它直接返回数字 4 → 布局系统据此给预览栏预留空间。
调用关系:它和 render 一起构成这个可绘制组件对外提供的能力:一个说明要多大,一个负责实际画内容。
centered_text_area140–144 ↗
tests::centered_text_area_centers_vertically151–163 ↗
fn centered_text_area_centers_vertically()
作用:这是一个测试,检查 centered_text_area 是否真的把文字区域放到了垂直居中的位置。它防止以后改代码时不小心把布局算歪。
数据流:进去的是一组固定的外层区域和文字高度 → 测试调用 centered_text_area 得到实际结果 → 再用断言比较它和预期矩形是否完全一致;一致就通过,不一致就失败。
调用关系:它只在测试时运行,不参与正常界面显示。它保护的是 centered_text_area 这个被 render 依赖的小计算函数。
调用图:外部调用 1 个(assert_eq!)。
辅助选择器和导入帮助器
这些文件实现相邻的交互式帮助器,用于平台操作、剪贴板和外部导入,以及独立主题选择。
tui/src/app/platform_actions.rs源码 ↗
这个文件主要解决两件事。第一,在 Windows 上,程序需要检查工作目录或相关路径里有没有“所有人都能写”的危险位置;这种目录就像一间谁都能进来改东西的仓库,可能影响沙箱安全。这里会把检查丢到后台线程做,避免卡住界面;如果检查失败,就发一个事件,让界面弹出警告。第二,它定义了侧边对话返回用的快捷键判断:只有按下 Ctrl+C 或 Ctrl+D 才算匹配,松开键或按 Esc 都不算。文件里还有一个 WindowsSandboxState,用来记住 Windows 沙箱检查是否开始过,以及是否要跳过下一次“全员可写目录”扫描。整体上,它像 App 的一小块平台适配板:平时不显眼,但能保证界面响应、快捷键行为和 Windows 安全提示都按预期工作。
App::spawn_world_writable_scan17–49 ↗
fn spawn_world_writable_scan(
cwd: AbsolutePathBuf,
workspace_roots: Vec<AbsolutePathBuf>,
env_map: std::collections::HashMap<String, String>,
logs_base_dir: AbsolutePa
作用:这个函数只在 Windows 上编译使用,用来启动一次“全员可写目录”安全扫描。它把可能比较慢的检查放到后台跑,避免文字界面被卡住。
数据流:进去的是当前目录、工作区根目录、环境变量、日志目录、权限配置,以及一个能给 App 发消息的发送器。它先把权限配置转换成 Windows 沙箱真正能用的权限规则;如果转换失败,就直接不做。转换成功后,它启动一个后台阻塞任务,在里面扫描相关路径并应用拒绝规则;如果扫描出错,它不会返回扫描细节,而是通过事件发送器通知界面:扫描失败,需要显示警告。
调用关系:这是 App 在 Windows 安全检查流程里的启动按钮。它先调用 try_from_permission_profile_for_workspace_roots 把用户/工作区权限变成可执行的沙箱权限,再交给 spawn_blocking 把重活放到后台。后台扫描失败时,它会转去调用 send_world_writable_scan_failed 发出界面事件。
调用图:调用 1 个内部函数(try_from_permission_profile_for_workspace_roots);外部调用 1 个(spawn_blocking)。
send_world_writable_scan_failed53–61 ↗
fn send_world_writable_scan_failed(tx: &AppEventSender)
作用:这个函数只在 Windows 上使用,用来告诉界面:“全员可写目录扫描失败了,请弹出警告”。它把失败信息包装成一个 App 事件发出去。
数据流:进去的是一个 AppEventSender,也就是给应用主循环递消息的通道。它创建一个 OpenWorldWritableWarningConfirmation 事件,里面不带示例路径,标记 failed_scan 为 true。然后它把这个事件发送出去;结果是界面后续能收到消息并显示对应提示。
调用关系:它是 Windows 扫描失败后的通知出口。App::spawn_world_writable_scan 在后台检查发现失败时会调用它;它再通过 send 把事情交给 App 的事件系统,让真正的界面流程去处理弹窗或提示。
side_return_shortcut_matches63–74 ↗
fn side_return_shortcut_matches(key_event: KeyEvent) -> bool
作用:这个函数判断一次键盘事件是不是“从侧边对话返回”的快捷键。它只认按下 Ctrl+C 或 Ctrl+D,并且大小写都算。
数据流:进去的是一个 KeyEvent,也就是一次键盘动作。它检查这次动作是不是按键按下、按的是字符键、同时带有 Control 修饰键,并且字符是 c/C 或 d/D。符合就返回 true,不符合就返回 false;它不修改任何状态。
调用关系:它是按键处理流程里的小裁判。别的界面代码在收到键盘事件时会用它判断是否应该触发“返回”动作;它内部只用 matches! 这种模式匹配写法来做判断,不把工作再交给别的项目函数。
调用图:外部调用 1 个(matches!)。
tests::side_return_shortcuts_match_ctrl_c_and_ctrl_d81–108 ↗
fn side_return_shortcuts_match_ctrl_c_and_ctrl_d()
作用:这是测试函数,用来保证侧边返回快捷键的判断不会被以后改坏。它确认 Ctrl+C、Ctrl+D 能匹配,而 Esc 不能匹配。
数据流:它自己构造几组键盘事件:小写和大写的 Ctrl+C、Ctrl+D,以及按下或松开的 Esc。然后逐个调用 side_return_shortcut_matches 做判断,并用 assert! 检查结果是否符合预期。测试通过时没有输出;如果逻辑错了,测试会失败,提醒开发者。
调用关系:它位于测试模块里,只在跑测试时参与。它围绕 side_return_shortcut_matches 做验证,像给这个快捷键规则加了一道保险,防止后续改代码时误把 Esc 或其他按键也当成返回快捷键。
调用图:外部调用 1 个(assert!)。
tui/src/clipboard_paste.rs源码 ↗
这个文件像一个“粘贴内容翻译员”。用户从浏览器、文件管理器、Windows、Linux、WSL 或安卓环境里复制东西后,程序不能直接假设它一定是干净的图片或路径。这里先定义了粘贴图片可能失败的原因,比如剪贴板打不开、里面没有图片、图片编码失败、临时文件写不进去。正常桌面系统上,它会尝试从系统剪贴板拿图片:有时剪贴板给的是文件列表,有时给的是一块图片像素数据;拿到后统一转成 PNG。需要文件时,它还会写到一个临时 PNG 文件里。Linux 下如果检测到 WSL(Windows 里的 Linux 环境),还会绕道调用 Windows PowerShell 去读 Windows 剪贴板。除此之外,它还会把粘贴的搜索词压成一行,把 file:// 地址、带引号路径、Windows 路径、shell 转义路径整理成真正的文件路径,并根据扩展名判断图片大概是 PNG、JPEG 还是别的格式。
PasteImageError::fmt14–21 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:把“粘贴图片失败”的内部错误,变成普通人能看懂的一句话。这样日志或界面里不会只显示生硬的错误类型。
数据流:进去的是一个具体错误,比如剪贴板不可用、没有图片、编码失败或文件读写失败,以及一个用于写文字的输出口;它按错误种类拼出说明文字;出来的是格式化后的错误信息,供显示或记录使用。
调用关系:它是 PasteImageError 这个错误类型的展示出口。其他函数遇到失败时会造出 PasteImageError,真正需要把错误打印出来时,就会走到这个格式化函数,并把文字交给 Rust 的写入机制。
调用图:外部调用 1 个(write!)。
EncodedImageFormat::label33–39 ↗
fn label(self) -> &'static str
作用:把图片格式枚举变成短标签,比如 PNG、JPEG、IMG。它适合给界面或提示信息使用。
数据流:进去的是一个图片格式值;它判断这个值是 PNG、JPEG 还是其他;出来的是一个固定的简短英文标签。
调用关系:它服务于 EncodedImageFormat 这个小类型。其他地方拿到图片格式后,如果需要给人看,而不是给机器判断,就可以调用它。
paste_image_as_png113–117 ↗
fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError>
作用:从系统剪贴板里拿一张图片,并统一转成 PNG 字节。这样后面的代码不用管图片原来来自 Chrome、Finder 还是别的软件。
数据流:进去没有业务参数,但它会读取系统剪贴板;它先试着把剪贴板里的文件当图片打开,失败后再试着读取剪贴板里的原始图片像素,然后把图片编码成 PNG;出来的是 PNG 的字节数组和图片宽高等信息。安卓上则不会真的读取,而是直接返回“不支持”的错误。
调用关系:它是图片粘贴的第一步。paste_image_to_temp_png 会先调用它拿 PNG 数据;如果在 Linux 下失败,外层可能再交给 WSL 备用方案处理。它内部会借助剪贴板库、图片库和日志记录工具完成读取、转换和调试记录。
调用图:调用 1 个内部函数(new);被 1 处调用(paste_image_to_temp_png);外部调用 8 个(new, new, ImageRgba8, from_raw, debug!, debug_span!, ClipboardUnavailable, EncodeFailed)。
try_wsl_clipboard_fallback159–193 ↗
fn try_wsl_clipboard_fallback(
error: &PasteImageError,
) -> Result<(PathBuf, PastedImageInfo), PasteImageError>
作用:在 WSL 里普通 Linux 剪贴板读不到 Windows 剪贴板时,尝试走一条备用路。简单说,就是请 Windows 那边帮忙把剪贴板图片存成文件。
数据流:进去的是前面读取剪贴板失败的错误;它先判断当前是不是很可能在 WSL,以及这个错误是否值得重试;然后调用 PowerShell 导出图片、把 Windows 路径转成 WSL 能访问的 /mnt/c/... 路径,并读取图片尺寸;出来的是图片文件路径和图片信息,或者保留原错误。
调用关系:它只在 Linux 版本的 paste_image_to_temp_png 失败后登场。它会调用 is_probably_wsl 判断环境,调用 try_dump_windows_clipboard_image 让 Windows 保存图片,再调用 convert_windows_path_to_wsl 把路径翻译给 Linux 使用。
调用图:调用 3 个内部函数(convert_windows_path_to_wsl, is_probably_wsl, try_dump_windows_clipboard_image);被 1 处调用(paste_image_to_temp_png);外部调用 4 个(image_dimensions, matches!, debug!, clone)。
try_dump_windows_clipboard_image199–229 ↗
fn try_dump_windows_clipboard_image() -> Option<String>
作用:尝试运行 Windows PowerShell,把 Windows 剪贴板里的图片保存成临时 PNG 文件。它是 WSL 备用粘贴方案里真正“拜托 Windows 干活”的部分。
数据流:进去没有参数;它准备一段 PowerShell 脚本,依次尝试 powershell.exe、pwsh、powershell 这些命令;如果某个命令成功保存图片,就读取命令输出里的 Windows 文件路径;出来是这个路径字符串,失败或没有图片时返回空。
调用关系:它由 try_wsl_clipboard_fallback 调用。它不负责判断是不是 WSL,也不负责把路径转成 Linux 路径,只负责尝试让 PowerShell 导出剪贴板图片。
调用图:被 1 处调用(try_wsl_clipboard_fallback);外部调用 3 个(from_utf8_lossy, new, debug!)。
paste_image_to_temp_png232–237 ↗
fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError>
作用:把剪贴板里的图片保存成一个临时 PNG 文件,并返回文件路径。这样后面的流程可以像处理普通图片文件一样处理粘贴来的图片。
数据流:进去没有业务参数,但它会读取剪贴板;它先调用 paste_image_as_png 得到 PNG 字节,然后创建一个唯一的临时文件,把字节写进去,并保留这个文件;出来的是临时文件路径和图片信息。安卓上直接返回不支持;Linux 下如果常规读取失败,会尝试 WSL 备用方案。
调用关系:它是图片粘贴到文件路径的主入口。它把底层剪贴板读取交给 paste_image_as_png,把 WSL 特殊情况交给 try_wsl_clipboard_fallback,把磁盘写入交给临时文件和文件系统工具。
调用图:调用 2 个内部函数(paste_image_as_png, try_wsl_clipboard_fallback);外部调用 3 个(new, write, ClipboardUnavailable)。
normalize_pasted_search_query240–243 ↗
fn normalize_pasted_search_query(pasted: &str) -> Option<String>
作用:把粘贴进搜索框的文字整理成单行搜索词。这样用户复制多行文字或带很多空格时,搜索不会被奇怪的换行打乱。
数据流:进去的是一段粘贴文本;它把所有连续空白字符,包括空格、换行、制表符,都压成单个空格,并去掉头尾空白;出来是整理后的搜索字符串,如果整理完为空就返回空值。
调用关系:它会被 handle_paste 这类粘贴处理流程调用。它不关心图片或路径,只专门服务于“把粘贴文本当搜索词”的场景。
调用图:被 2 处调用(handle_paste, handle_paste)。
normalize_pasted_path251–287 ↗
fn normalize_pasted_path(pasted: &str) -> Option<PathBuf>
作用:把用户粘贴的“看起来像文件路径”的文字,整理成程序能使用的路径。它能处理 file:// 地址、带引号路径、Windows 路径和带反斜杠转义的 shell 路径。
数据流:进去的是原始粘贴文字;它先去掉首尾空白和简单引号,再尝试把 file:// URL 转成本地路径;如果像 Windows 路径,就交给 normalize_windows_path;否则用 shlex(一种按命令行规则拆字的工具)尝试把单个被转义的路径还原;出来是 PathBuf 路径对象,若发现粘贴内容其实是多个东西就返回空。
调用关系:它是处理粘贴图片路径时的核心清洗步骤,会被 handle_paste_image_path 调用,也被很多测试覆盖。它把 Windows 路径的特殊判断交给 normalize_windows_path。
调用图:调用 1 个内部函数(normalize_windows_path);被 12 处调用(handle_paste_image_path, normalize_double_quoted_windows_path, normalize_file_url, normalize_file_url_windows, normalize_multiple_tokens_returns_none, normalize_shell_escaped_single_path, normalize_simple_quoted_path_fallback, normalize_single_quoted_unix_path, normalize_single_quoted_windows_path, normalize_unc_windows_path (+2 more));外部调用 3 个(from, new, parse)。
is_probably_wsl290–303 ↗
fn is_probably_wsl() -> bool
作用:判断当前 Linux 程序是不是跑在 WSL 里。WSL 是 Windows Subsystem for Linux,也就是 Windows 里的 Linux 环境,路径和剪贴板都常常需要特殊处理。
数据流:进去没有参数;它先读取 /proc/version 看里面有没有 microsoft 或 WSL 字样,再检查 WSL_DISTRO_NAME、WSL_INTEROP 这些环境变量;出来是真或假,表示当前环境是否很像 WSL。
调用关系:它会被剪贴板备用方案、路径转换、界面快捷键判断和相关测试使用。它的作用像一个环境探测器,告诉其他代码要不要按 WSL 的规则办事。
调用图:被 11 处调用(footer_props, paste_image_shortcut_prefers_ctrl_alt_v_under_wsl, is_wsl_session, normalize_windows_path, normalize_double_quoted_windows_path, normalize_file_url_windows, normalize_single_quoted_windows_path, normalize_unquoted_windows_path_with_spaces, normalize_windows_path_in_wsl, try_wsl_clipboard_fallback (+1 more));外部调用 2 个(var_os, read_to_string)。
convert_windows_path_to_wsl306–331 ↗
fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf>
作用:把 Windows 盘符路径转换成 WSL 里的 Linux 路径。比如 C:\Users\Alice 会变成 /mnt/c/Users/Alice。
数据流:进去的是一个 Windows 风格路径字符串;它拒绝网络共享 UNC 路径,检查开头是不是盘符加冒号,然后把盘符变成 /mnt/盘符,并把反斜杠分隔的每一段追加进去;出来是 WSL 可访问的 PathBuf,格式不合适就返回空。
调用关系:它被 normalize_windows_path 和 try_wsl_clipboard_fallback 调用。前者用于整理用户粘贴的路径,后者用于把 PowerShell 导出的 Windows 临时文件路径交给 WSL 读取。
调用图:被 6 处调用(normalize_windows_path, normalize_double_quoted_windows_path, normalize_file_url_windows, normalize_single_quoted_windows_path, normalize_unquoted_windows_path_with_spaces, try_wsl_clipboard_fallback);外部调用 2 个(from, format!)。
normalize_windows_path333–361 ↗
fn normalize_windows_path(input: &str) -> Option<PathBuf>
作用:识别并整理 Windows 风格的路径,包括 C:\... 这种盘符路径和 \\server\share 这种网络共享路径。它避免普通 Unix 解析规则把反斜杠误当成转义符。
数据流:进去的是一段路径字符串;它先判断是不是 Windows 盘符路径或 UNC 网络路径;如果在 Linux 且当前是 WSL,就尝试把盘符路径转换成 /mnt/...;出来是整理后的 PathBuf,不像 Windows 路径则返回空。
调用关系:它是 normalize_pasted_path 的辅助零件,专门处理 Windows 路径这个麻烦分支。它需要 is_probably_wsl 判断环境,也可能调用 convert_windows_path_to_wsl 做实际转换。
调用图:调用 2 个内部函数(convert_windows_path_to_wsl, is_probably_wsl);被 1 处调用(normalize_pasted_path);外部调用 1 个(from)。
pasted_image_format364–375 ↗
fn pasted_image_format(path: &Path) -> EncodedImageFormat
作用:根据文件扩展名猜测粘贴的图片是什么格式。比如 .png 算 PNG,.jpg 和 .jpeg 算 JPEG,其他就归为普通图片。
数据流:进去的是一个文件路径;它取出扩展名并转成小写,然后匹配 png、jpg、jpeg;出来是 EncodedImageFormat 里的 PNG、JPEG 或 Other。
调用关系:它会被 handle_paste_image_path 用来给粘贴的图片路径补充格式信息。它只看文件名后缀,不打开文件验证内容。
调用图:被 1 处调用(handle_paste_image_path);外部调用 1 个(extension)。
pasted_search_query_tests::collapses_whitespace382–387 ↗
fn collapses_whitespace()
作用:检查搜索词整理函数是否真的能把多种空白压成普通空格。这个测试防止以后改代码时把多行粘贴搜索弄坏。
数据流:进去的是一段带空格、换行、制表符的测试字符串;测试调用 normalize_pasted_search_query;出来的期望结果是 alpha beta gamma,如果不一致测试就失败。
调用关系:它是 normalize_pasted_search_query 的单元测试。平时运行测试套件时执行,不参与真实用户粘贴流程。
调用图:外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_file_url396–400 ↗
fn normalize_file_url()
作用:检查 file:// 形式的本地文件地址能不能被转成普通路径。比如 file:///tmp/example.png 应该变成 /tmp/example.png。
数据流:进去的是一个 file:// 测试字符串;测试调用 normalize_pasted_path;出来的路径要和预期 PathBuf 相同,否则测试失败。
调用关系:它是 normalize_pasted_path 的测试之一,只在非 Windows 环境运行,用来保证粘贴文件 URL 的场景可靠。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_file_url_windows403–417 ↗
fn normalize_file_url_windows()
作用:检查 Windows 盘符路径能不能被识别。它还兼顾 WSL:如果在 WSL 中,预期结果可能是 /mnt/c/... 形式。
数据流:进去的是 C:\Temp\example.png 这样的测试路径;测试调用 normalize_pasted_path,并在 Linux 下根据 is_probably_wsl 和 convert_windows_path_to_wsl 算出应有结果;最后比较实际路径和预期路径。
调用关系:它覆盖 normalize_pasted_path、is_probably_wsl 和 convert_windows_path_to_wsl 之间的配合,确保 Windows 路径在普通系统和 WSL 中都按预期处理。
调用图:调用 3 个内部函数(convert_windows_path_to_wsl, is_probably_wsl, normalize_pasted_path);外部调用 2 个(from, assert_eq!)。
pasted_paths_tests::normalize_shell_escaped_single_path420–424 ↗
fn normalize_shell_escaped_single_path()
作用:检查带 shell 转义的单个 Unix 路径能不能还原。shell 转义就是命令行里用反斜杠表示空格这类写法。
数据流:进去的是 /home/user/My\ File.png;测试调用 normalize_pasted_path;出来应该是包含真实空格的 /home/user/My File.png。
调用关系:它验证 normalize_pasted_path 里使用 shlex 拆解单个路径的分支,保证用户从终端复制路径时不会因为反斜杠而找不到文件。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_simple_quoted_path_fallback427–431 ↗
fn normalize_simple_quoted_path_fallback()
作用:检查双引号包住的路径能不能去掉外层引号。很多文件管理器或用户手动复制时会带这种引号。
数据流:进去的是 " /home/user/My File.png " 这种外层带双引号的测试路径;测试调用 normalize_pasted_path;出来应该是不带引号、保留中间空格的路径。
调用关系:它验证 normalize_pasted_path 开头的简单去引号逻辑,防止带空格路径被误拆成多个片段。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_single_quoted_unix_path434–438 ↗
fn normalize_single_quoted_unix_path()
作用:检查单引号包住的 Unix 路径能不能正常变成路径对象。单引号是命令行里常见的保护空格写法。
数据流:进去的是 '/home/user/My File.png';测试调用 normalize_pasted_path;出来应该是 /home/user/My File.png 这个路径。
调用关系:它是 normalize_pasted_path 的引号处理测试,和双引号测试一起保证常见复制路径格式都能被接受。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_multiple_tokens_returns_none441–446 ↗
fn normalize_multiple_tokens_returns_none()
作用:检查如果用户一次粘贴了两个路径,函数不会装作这是一个路径。这样可以避免程序误用半截内容。
数据流:进去的是两个路径连在一起的测试字符串;normalize_pasted_path 用命令行规则拆开后发现不止一个片段;出来应该是空值,测试用断言确认这一点。
调用关系:它验证 normalize_pasted_path 的安全边界:只接受单个路径。真实粘贴图片路径时,这能减少误判。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert!)。
pasted_paths_tests::pasted_image_format_png_jpeg_unknown449–470 ↗
fn pasted_image_format_png_jpeg_unknown()
作用:检查根据扩展名判断图片格式的规则是否正确。PNG、jpg、jpeg 要认出来,没扩展名或 webp 这类未知格式要归为 Other。
数据流:进去的是多组测试路径;测试分别调用 pasted_image_format;出来的格式枚举要和预期一致,否则测试失败。
调用关系:它直接保护 pasted_image_format 的后缀判断规则,避免以后改动时把大小写或 JPEG 的两种扩展名处理弄坏。
调用图:外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_single_quoted_windows_path473–489 ↗
fn normalize_single_quoted_windows_path()
作用:检查单引号包住的 Windows 路径能不能被识别,并在 WSL 中按需要转换。这个场景常见于从命令行或聊天里复制路径。
数据流:进去的是带单引号的 C:\Users\Alice\My File.jpeg;测试调用 normalize_pasted_path;Linux 下如果是 WSL,会用 convert_windows_path_to_wsl 计算预期,否则保留 Windows 路径;最后比较结果。
调用关系:它覆盖 normalize_pasted_path 对“引号 + Windows 路径 + 可能是 WSL”的组合处理,确保多个特殊规则叠在一起时也不冲突。
调用图:调用 3 个内部函数(convert_windows_path_to_wsl, is_probably_wsl, normalize_pasted_path);外部调用 2 个(from, assert_eq!)。
pasted_paths_tests::normalize_double_quoted_windows_path492–508 ↗
fn normalize_double_quoted_windows_path()
作用:检查双引号包住的 Windows 路径能不能被识别。它和单引号测试类似,但覆盖另一种常见粘贴格式。
数据流:进去的是带双引号的 Windows 路径;测试调用 normalize_pasted_path;再根据是否 WSL 算出应有路径;出来结果必须和预期相等。
调用关系:它验证 normalize_pasted_path、is_probably_wsl、convert_windows_path_to_wsl 的组合行为,特别是双引号去除后仍能识别 Windows 路径。
调用图:调用 3 个内部函数(convert_windows_path_to_wsl, is_probably_wsl, normalize_pasted_path);外部调用 2 个(from, assert_eq!)。
pasted_paths_tests::normalize_unquoted_windows_path_with_spaces511–525 ↗
fn normalize_unquoted_windows_path_with_spaces()
作用:检查没有引号、但中间有空格的 Windows 路径能不能被当成一个完整路径。否则 My Pictures 这类目录名很容易被误拆。
数据流:进去的是 C:\Users\Alice\My Pictures\example image.png;测试调用 normalize_pasted_path;如果在 WSL 就期待 /mnt/c/...,否则期待原 Windows 路径;最后做相等检查。
调用关系:它特别验证 normalize_pasted_path 会优先识别 Windows 路径,而不是先按 Unix 命令行规则把空格拆开。
调用图:调用 3 个内部函数(convert_windows_path_to_wsl, is_probably_wsl, normalize_pasted_path);外部调用 2 个(from, assert_eq!)。
pasted_paths_tests::normalize_unc_windows_path528–535 ↗
fn normalize_unc_windows_path()
作用:检查 Windows 网络共享路径,也就是 UNC 路径,能不能被接受。UNC 路径长得像 \\server\share\folder\file.jpg。
数据流:进去的是一个 UNC 测试路径;测试调用 normalize_pasted_path;出来应该保持为同一个 UNC 路径对象。
调用关系:它覆盖 normalize_windows_path 对网络共享路径的识别。和盘符路径不同,UNC 路径不会被转换成 WSL 的 /mnt/...。
调用图:调用 1 个内部函数(normalize_pasted_path);外部调用 1 个(assert_eq!)。
pasted_paths_tests::pasted_image_format_with_windows_style_paths538–551 ↗
fn pasted_image_format_with_windows_style_paths()
作用:检查 Windows 风格路径里的扩展名也能被 pasted_image_format 正确识别。路径分隔符不同不应该影响格式判断。
数据流:进去的是几组带反斜杠的 Windows 风格路径;测试调用 pasted_image_format;出来应分别是 PNG、JPEG 或 Other。
调用关系:它保护 pasted_image_format 在跨平台路径上的表现,确保处理图片路径时不会只适用于 Unix 风格路径。
调用图:外部调用 1 个(assert_eq!)。
pasted_paths_tests::normalize_windows_path_in_wsl555–567 ↗
fn normalize_windows_path_in_wsl()
作用:检查真实 WSL 环境里 Windows 路径是否会被转换成 /mnt/c/...。如果不是 WSL,这个测试会主动跳过。
数据流:进去的是 C:\Users\Alice\Pictures\example image.png;测试先调用 is_probably_wsl,不是 WSL 就直接结束;如果是 WSL,就调用 normalize_pasted_path,并期待得到 /mnt/c/Users/Alice/Pictures/example image.png。
调用关系:它是 WSL 专用的路径转换测试,验证 normalize_pasted_path 通过 normalize_windows_path 和 convert_windows_path_to_wsl 连起来后的真实效果。
调用图:调用 2 个内部函数(is_probably_wsl, normalize_pasted_path);外部调用 1 个(assert_eq!)。
tui/src/external_agent_config_migration_flow.rs源码 ↗
这个文件像一个“导入向导”的总调度员。用户触发 /import 后,它先检查几件容易出错的事:现在是不是远程会话、是不是连着本地守护进程、是不是已经有一次导入在跑。只要条件不合适,就直接给出清楚的提示,避免用户点了半天却失败。条件通过后,它会让 app server(应用后台服务)扫描 Claude Code 的配置,看看有没有可导入的项目。没有就告诉用户没有东西可导入;有的话,就打开 TUI(终端界面)里的选择提示,让用户决定导入哪些。用户确认后,它把选择交给后台执行导入。如果导入失败,会把错误带回选择界面,让用户能重试或取消。导入成功时,它还会告诉用户:导入已经开始,可以继续工作,并提醒是否还有剩余项目之后再导入。
external_agent_config_migration_success_message22–28 ↗
fn external_agent_config_migration_success_message(remaining_item_count: usize) -> String
作用:这个函数用来生成“导入已开始”的成功提示。它还会根据有没有剩余项目,顺手补一句下一步该怎么做。
数据流:进去的是剩余未导入项目的数量 → 它先准备一段固定的成功文案,再调用 remaining_items_handoff 看需不需要追加“还有几个项目”的提醒 → 出来的是一整句给用户看的提示文字,不改动外部状态。
调用关系:它是在 handle_external_agent_config_migration_prompt 成功启动导入后被调用的。它把“剩余项目怎么说”这个小细节交给 remaining_items_handoff,自己负责拼成最终展示给用户的消息。
调用图:调用 1 个内部函数(remaining_items_handoff);被 1 处调用(handle_external_agent_config_migration_prompt);外部调用 1 个(format!)。
remaining_items_handoff30–41 ↗
fn remaining_items_handoff(remaining_item_count: usize) -> Option<String>
作用:这个函数专门把“还剩几个项目没导入”翻译成适合人看的提示。没有剩余就不说,有一个和有多个时用不同说法,避免提示显得别扭。
数据流:进去的是一个数字,也就是剩余项目数量 → 它判断数字是 0、1,还是更多 → 出来的是可选文字:0 时没有额外提示,1 时说还剩 1 个,多个时说还剩多少个;它不修改任何东西。
调用关系:它只服务于 external_agent_config_migration_success_message。主流程不直接关心怎么措辞,只需要最后拿到一条好懂的成功提示。
调用图:被 1 处调用(external_agent_config_migration_success_message);外部调用 1 个(format!)。
handle_external_agent_config_migration_prompt43–120 ↗
async fn handle_external_agent_config_migration_prompt(
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
config: &Config,
) -> Result<ExternalAgentConfigMigrationFlowOutcome, String>
作用:这是这个文件的主流程函数,负责从用户触发导入开始,到检查环境、扫描项目、弹出选择界面、提交导入、返回结果的全过程。有人运行 /import 时,核心工作就是它来串起来的。
数据流:进去的是终端界面 tui、应用后台连接 app_server、以及配置 config(主要用当前工作目录)→ 它先读 app_server 的状态,确认不是远程会话、不是守护进程模式、也没有导入正在进行;然后用当前目录和用户主目录去请求后台扫描 Claude Code 配置;如果发现项目,就把项目列表交给选择界面让用户确认;用户确认后,再把选中的项目交给后台开始导入 → 出来的是一个流程结果:已开始并带成功提示、没有可导入项目、用户取消,或者一条错误消息;过程中可能会记录警告日志,并可能启动一次后台导入。
调用关系:它由 handle_event 在用户触发相关命令时调用,是整个导入功能的入口调度点。它会向 app_server 询问环境状态和扫描结果,也会调用 run_external_agent_config_migration_prompt 让用户做选择;导入成功后,它再调用 external_agent_config_migration_success_message 生成最终提示。
调用图:调用 7 个内部函数(external_agent_config_detect, external_agent_config_import, external_agent_config_import_in_progress, uses_embedded_app_server, uses_remote_workspace, run_external_agent_config_migration_prompt, external_agent_config_migration_success_message);被 1 处调用(handle_event);外部调用 4 个(format!, warn!, Started, vec!)。
tui/src/theme_picker.rs源码 ↗
这个文件解决的是“选主题不能靠猜”的问题。它会把内置主题和用户放在 {CODEX_HOME}/themes/ 里的自定义 .tmTheme 文件列出来,做成一个可搜索的选择弹窗。用户移动光标时,它马上把当前运行中的语法高亮主题临时换掉,让右侧或下方的示例代码跟着变色;如果按确认,就发出选择主题的事件,后续会写入配置;如果按取消,就把打开弹窗前的主题换回来。文件里还有两种预览:宽屏时放在右侧,窄屏时压缩成 4 行放在列表下方。它像试衣间:试穿时马上能看效果,不买就换回原来的衣服。
preview_diff_line_type148–154 ↗
fn preview_diff_line_type(kind: PreviewDiffKind) -> DiffLineType
作用:把预览行的简单分类转换成真正 diff 渲染器认识的类型。diff 可以理解成代码改动对比,通常有普通行、新增行、删除行。
数据流:进去的是一个预览行种类:普通、新增或删除 → 它按固定对应关系翻译 → 出来的是渲染系统能用的 diff 行类型,比如新增会变成 Insert,删除会变成 Delete。
调用关系:它是 render_preview 的小翻译员。预览在画每一行前会先问它:这一行该按普通、加号还是减号来显示。
调用图:被 1 处调用(render_preview)。
centered_offset156–164 ↗
fn centered_offset(available: u16, content: u16, min_frame: u16) -> u16
作用:计算内容在一块区域里垂直居中时,上面应该空出多少行。它还会尽量留一点边框感,不让内容贴得太死。
数据流:进去的是可用高度、内容高度、最少留白 → 它先算出空闲空间,再决定是否留出上下边距 → 出来的是顶部要跳过的行数。
调用关系:它被 render_preview 用在宽屏预览里,让示例代码在右侧面板中间显示,而不是总贴在顶部。
调用图:被 1 处调用(render_preview)。
render_preview166–235 ↗
fn render_preview(
area: Rect,
buf: &mut Buffer,
preview_rows: &[PreviewRow],
center_vertically: bool,
left_inset: u16,
)
作用:真正把主题预览画到终端缓冲区里。它会显示带行号、加减号和语法高亮的 Rust 代码片段。
数据流:进去的是要画的屏幕区域、终端画布、预览行列表、是否居中、左侧缩进 → 它先把代码交给高亮器染色,再按 diff 样式加上行号和加减标记,并只取每行的第一屏内容画出来 → 出来的是被写入内容的终端缓冲区;如果区域太小或没有行,就什么也不画。
调用关系:宽版和窄版预览的 render 都把活交给它。它内部会调用语法高亮、diff 样式、行号宽度和换行渲染这些底层工具,把一段示例代码画成和真实 diff 很像的预览。
调用图:调用 7 个内部函数(current_diff_render_style_context, line_number_width, push_wrapped_diff_line_with_style_context, push_wrapped_diff_line_with_syntax_and_style_context, highlight_code_to_styled_spans, centered_offset, preview_diff_line_type);被 2 处调用(render, render);外部调用 4 个(new, is_empty, iter, len)。
ThemePreviewWideRenderable::desired_height238–240 ↗
fn desired_height(&self, _width: u16) -> u16
作用:告诉布局系统:宽屏右侧预览希望尽量占满可用高度。
数据流:进去的是宽度,但这里不需要用 → 它直接返回最大的高度值 → 出来的是一个“我愿意占满高度”的信号。
调用关系:选择弹窗布局时会读取这个值,知道右侧预览面板可以撑满整块侧栏。
ThemePreviewWideRenderable::render242–250 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:画宽屏版本的主题预览,也就是放在列表右边的较长代码示例。
数据流:进去的是屏幕区域和画布 → 它把宽屏预览行、垂直居中、左侧 2 列缩进这些参数交给 render_preview → 出来的是右侧区域里出现一段带颜色的 diff 示例。
调用关系:它是宽屏预览组件的入口。弹窗在空间足够时会用它,而它自己不画细节,只调用 render_preview。
调用图:调用 1 个内部函数(render_preview)。
ThemePreviewNarrowRenderable::desired_height254–256 ↗
fn desired_height(&self, _width: u16) -> u16
作用:告诉布局系统:窄屏预览只需要固定的 4 行高度。
数据流:进去的是宽度,但这里不用 → 它返回窄版预览行数 → 出来的是布局系统可以预留的高度。
调用关系:当终端不够宽,弹窗要把预览叠在列表下面时,会用这个高度来安排空间。
ThemePreviewNarrowRenderable::render258–266 ↗
fn render(&self, area: Rect, buf: &mut Buffer)
作用:画窄屏版本的主题预览,用很短的 4 行代码展示普通、新增和删除效果。
数据流:进去的是屏幕区域和画布 → 它把窄屏预览行、不垂直居中、无左缩进这些参数交给 render_preview → 出来的是列表下方的一小块代码颜色预览。
调用关系:它是窄屏预览组件的入口。终端宽度不够做左右分栏时,弹窗会改用它。
调用图:调用 1 个内部函数(render_preview)。
subtitle_available_width269–281 ↗
fn subtitle_available_width(terminal_width: Option<u16>) -> usize
作用:估算主题弹窗副标题最多能写多宽,避免提示文字太长挤坏界面。
数据流:进去的是终端宽度,可能没有;没有就按 80 列算 → 它先算弹窗内容宽度,再看是否能左右分栏;能分栏就用列表那边的宽度,不能就用整块内容宽度 → 出来的是副标题可用的字符宽度。
调用关系:它服务于 theme_picker_subtitle。副标题想显示自定义主题目录前,会先用它判断这句话放不放得下。
调用图:被 1 处调用(theme_picker_subtitle);外部调用 2 个(popup_content_width, side_by_side_layout_widths)。
theme_picker_subtitle283–300 ↗
fn theme_picker_subtitle(codex_home: Option<&Path>, terminal_width: Option<u16>) -> String
作用:生成主题选择弹窗下面那句提示文字。能放下时提示用户自定义主题该放在哪个目录,放不下时就提示可以上下移动预览。
数据流:进去的是 Codex 主目录和终端宽度 → 它尝试拼出 themes 目录,并把家目录显示成 ~ 这种短写;再检查整句宽度是否够放 → 出来的是目录提示,或者兜底的“上下移动可实时预览主题”。
调用关系:它被 build_theme_picker_params 用来填弹窗副标题,也被多条测试直接检查,确保不同宽度和路径情况下不会显示过长文字。
调用图:调用 1 个内部函数(subtitle_available_width);被 5 处调用(build_theme_picker_params, subtitle_falls_back_for_94_column_terminal_side_by_side_layout, subtitle_falls_back_to_preview_instructions_without_tilde_path, subtitle_falls_back_when_tilde_path_subtitle_is_too_wide, subtitle_uses_tilde_path_when_codex_home_under_home_directory);外部调用 2 个(width, format!)。
build_theme_picker_params312–410 ↗
fn build_theme_picker_params(
current_name: Option<&str>,
codex_home: Option<&Path>,
terminal_width: Option<u16>,
) -> SelectionViewParams
作用:组装 /theme 弹窗所需的全部参数:标题、列表项、搜索、预览、确认动作、取消恢复动作都在这里配好。
数据流:进去的是当前配置里的主题名、Codex 主目录、终端宽度 → 它先保存当前主题快照,再列出可用主题;然后决定初始选中哪一项,把每个主题变成可点击的列表项;接着设置移动光标时临时换主题、取消时恢复旧主题、确认时发送选择事件 → 出来的是一个完整的 SelectionViewParams,弹窗系统拿它就能显示和运行主题选择器。
调用关系:这是本文件的主入口。打开 /theme 时外部会调用它;它把主题发现、高亮切换、预览组件、提示文字和事件发送串成一个完整流程。
调用图:调用 5 个内部函数(standard_popup_hint_line, configured_theme_name, current_syntax_theme, list_available_themes, theme_picker_subtitle);被 6 处调用(theme_picker_enables_side_content_background_preservation, theme_picker_subtitle_uses_fallback_text_in_94x35_terminal, open_theme_picker, theme_picker_items_include_search_values_for_preview_mapping, theme_picker_uses_half_width_with_stacked_fallback_preview, unavailable_configured_theme_falls_back_to_configured_or_default_selection);外部调用 2 个(new, default)。
tests::render_buffer418–423 ↗
tests::render_lines425–441 ↗
fn render_lines(renderable: &dyn Renderable, width: u16, height: u16) -> Vec<String>
作用:测试用的小工具:把渲染结果转成一行行普通字符串,方便用断言检查文字位置。
数据流:进去的是组件、宽度和高度 → 它先调用 tests::render_buffer 得到画布,再逐格读取字符,空格补成普通空格 → 出来的是字符串数组,每个字符串代表终端里的一行。
调用关系:预览布局测试主要用它检查行号、加减号、缩进和居中效果。
调用图:外部调用 1 个(render_buffer)。
tests::first_non_space_style_after_marker443–452 ↗
fn first_non_space_style_after_marker(buf: &Buffer, row: u16, width: u16) -> Option<Modifier>
作用:测试用的小工具:找到某行加号或减号后面第一个非空字符的样式。它用来确认删除行真的被做了变暗处理。
数据流:进去的是缓冲区、行号、宽度 → 它先找这一行里的 + 或 - 标记,再往右找第一个不是空格的字符 → 出来的是这个字符的样式修饰;找不到就返回空。
调用关系:它被删除行样式测试调用,用来验证预览和真实 diff 渲染保持一致。
tests::preview_line_number454–465 ↗
fn preview_line_number(line: &str) -> Option<usize>
作用:测试用的小工具:从一行预览文字开头读出行号。
数据流:进去的是一行字符串 → 它去掉左侧空白,读取最前面的数字,并确认数字后面跟着空格 → 出来的是行号数字;格式不对就返回空。
调用关系:宽版和窄版预览测试用它判断哪些行是真的代码预览行,以及行号是否符合预期。
tests::preview_line_marker467–478 ↗
fn preview_line_marker(line: &str) -> Option<char>
作用:测试用的小工具:从一行预览文字里读出 diff 标记,也就是普通空格、加号或减号位置上的字符。
数据流:进去的是一行字符串 → 它先按预览格式跳过行号和后面的空格,再取下一个字符 → 出来的是该行的标记字符;格式不对就返回空。
调用关系:预览测试用它统计新增行和删除行,确保示例里真的有 + 和 -。
tests::theme_picker_uses_half_width_with_stacked_fallback_preview481–488 ↗
fn theme_picker_uses_half_width_with_stacked_fallback_preview()
作用:测试主题弹窗在宽屏时使用半宽侧栏,同时也准备了窄屏备用预览。
数据流:进去没有外部输入 → 它调用 build_theme_picker_params 生成参数 → 然后检查侧栏宽度策略、最小宽度和窄屏预览是否都设置好了。
调用关系:它保护弹窗布局规则,防止以后改代码时不小心丢掉右侧预览或窄屏兜底。
调用图:调用 1 个内部函数(build_theme_picker_params);外部调用 2 个(assert!, assert_eq!)。
tests::theme_picker_items_include_search_values_for_preview_mapping491–499 ↗
fn theme_picker_items_include_search_values_for_preview_mapping()
作用:测试每个主题列表项都带有真实主题名,确保光标移动时能找到对应主题来预览。
数据流:进去没有外部输入 → 它构建主题弹窗参数 → 检查所有列表项的 search_value 都不是空。
调用关系:它保护 build_theme_picker_params 里的预览映射逻辑,因为实时预览就是靠列表项里的这个主题名找主题。
调用图:调用 1 个内部函数(build_theme_picker_params);外部调用 1 个(assert!)。
tests::wide_preview_renders_all_lines_with_vertical_center_and_left_inset502–549 ↗
fn wide_preview_renders_all_lines_with_vertical_center_and_left_inset()
作用:测试宽屏预览会画出所有示例行,并且上下居中、左边留 2 列空白。
数据流:进去没有外部输入 → 它把宽屏预览画成字符串行 → 再找出带行号的行,检查数量、上下留白、开头缩进,以及是否包含新增和删除标记。
调用关系:它直接验证 ThemePreviewWideRenderable::render 和 render_preview 的宽屏布局效果。
调用图:外部调用 3 个(assert!, assert_eq!, render_lines)。
tests::narrow_preview_renders_single_add_and_single_remove_in_four_lines552–579 ↗
fn narrow_preview_renders_single_add_and_single_remove_in_four_lines()
作用:测试窄屏预览正好用 4 行,并且始终包含一行新增和一行删除。
数据流:进去没有外部输入 → 它渲染窄屏预览 → 读取行号和 diff 标记 → 检查行号顺序、加号数量、减号数量,以及第一行是否从左边开始。
调用关系:它保护窄屏兜底预览的核心要求:短,但仍能展示主题对增删行的效果。
调用图:外部调用 3 个(assert!, assert_eq!, render_lines)。
tests::deleted_preview_code_uses_dim_overlay_like_real_diff_renderer582–598 ↗
fn deleted_preview_code_uses_dim_overlay_like_real_diff_renderer()
作用:测试预览里的删除代码会像真实 diff 一样变暗。变暗能让用户一眼看出这是被删掉的内容。
数据流:进去没有外部输入 → 它渲染窄屏预览,找到删除行,再读取减号后第一个代码字符的样式 → 最后确认样式里包含变暗修饰。
调用关系:它把主题预览和真实 diff 渲染行为绑在一起,防止预览看起来和实际代码改动显示不一致。
调用图:外部调用 4 个(assert!, first_non_space_style_after_marker, render_buffer, render_lines)。
tests::subtitle_uses_tilde_path_when_codex_home_under_home_directory601–609 ↗
fn subtitle_uses_tilde_path_when_codex_home_under_home_directory()
作用:测试当 Codex 目录在用户家目录下面时,副标题会用 ~ 这种更短、更熟悉的写法。
数据流:进去没有外部输入 → 它取系统家目录,拼出 .codex 路径,再生成副标题 → 检查文字里包含 ~ 和目录提示。
调用关系:它验证 theme_picker_subtitle 在宽度足够时会给用户清楚的自定义主题目录说明。
调用图:调用 1 个内部函数(theme_picker_subtitle);外部调用 2 个(assert!, home_dir)。
tests::subtitle_falls_back_when_tilde_path_subtitle_is_too_wide612–620 ↗
fn subtitle_falls_back_when_tilde_path_subtitle_is_too_wide()
作用:测试即使用了 ~,如果路径提示还是太长,副标题也会退回到短提示。
数据流:进去没有外部输入 → 它构造一个很长的 Codex 路径,生成副标题 → 检查结果等于兜底预览提示。
调用关系:它保护界面不被过长路径撑坏,验证 theme_picker_subtitle 的宽度判断。
调用图:调用 1 个内部函数(theme_picker_subtitle);外部调用 2 个(assert_eq!, home_dir)。
tests::subtitle_falls_back_to_preview_instructions_without_tilde_path623–627 ↗
fn subtitle_falls_back_to_preview_instructions_without_tilde_path()
作用:测试没有 Codex 主目录时,副标题会显示通用的预览操作提示。
数据流:进去没有外部输入 → 它在没有目录信息的情况下生成副标题 → 检查结果是兜底提示文字。
调用关系:它覆盖 theme_picker_subtitle 的无路径场景,确保弹窗仍然有有用提示。
调用图:调用 1 个内部函数(theme_picker_subtitle);外部调用 1 个(assert_eq!)。
tests::subtitle_falls_back_for_94_column_terminal_side_by_side_layout630–637 ↗
fn subtitle_falls_back_for_94_column_terminal_side_by_side_layout()
作用:测试在 94 列这种比较尴尬的终端宽度下,副标题不会硬塞目录提示,而是使用短提示。
数据流:进去没有外部输入 → 它用家目录下的 .codex 和 94 列宽度生成副标题 → 检查结果是兜底提示。
调用关系:它验证 subtitle_available_width 和 theme_picker_subtitle 配合后的实际效果,尤其是左右分栏时列表区域变窄的情况。
调用图:调用 1 个内部函数(theme_picker_subtitle);外部调用 2 个(assert_eq!, home_dir)。