其它界面 · 登录引导、恢复选择、代理总览与用量面板
聊天界面之外,TUI 还有一圈界面:登录引导与目录信任、恢复选择器、代理总览、用量面板、终端宠物、主题与状态栏。它们按挂载方式分成三类:App::run 之前自带事件循环的启动界面,ChatWidget 底部视图栈里的弹窗,以及 App 在备用屏幕上开的覆盖层。本篇逐个点到,重点讲清它们挂在哪、数据从哪来。
其它界面 · 登录引导、恢复选择、代理总览与用量面板
前几篇围绕聊天主界面:事件循环、输入框、历史记录单元和全屏对话记录。codex-tui 里还有不少别的界面,每个都有自己的状态机,这一篇不深挖其中任何一个,只回答两个问题:它挂在哪,数据从哪来。
三种挂法
- 启动界面:在
App::run之前运行,从启动流程手里借用Tui,自己跑一个小的select!事件循环,结束后把结果交回启动流程。登录引导、会话选择器、目录信任确认都属于这一类。 - 底部视图:实现
BottomPaneViewtrait,压进ChatWidget底部的视图栈(见输入框一篇),不算测试有近 20 个实现,从选择列表、审批框到代理总览都是。 - 覆盖层:
App的overlay字段,类型Overlay,三个变体Transcript、Static、Analytics,打开时进入备用屏幕,关闭时回到原来的界面。
和前几篇一样,这些界面也尽量经 app-server 取数、写数,只有少数例外,下文逐个注明。
登录引导与目录信任
onboarding/ 是一个很小的步骤状态机,Step 只有 Welcome、Auth、TrustDirectory 三种。要不要登录,判定只有两个条件:
fn should_show_login_screen(login_status: LoginStatus, requires_openai_auth: bool) -> bool {
// Only show the login screen for providers that actually require OpenAI auth
// (OpenAI or equivalents). For OSS/other providers, skip login entirely.
if !requires_openai_auth {
return false;
}
login_status == LoginStatus::NotAuthenticated
}(codex-rs/tui/src/lib.rs:2296)
登录状态本身由 app-server 的账号接口给出。登录步骤(onboarding/auth.rs)管 ChatGPT 浏览器登录、设备码和 API key 三条路,都发 account/login/start,取消发 account/login/cancel。Amazon Bedrock 的设置向导嵌在登录步骤里(onboarding/bedrock.rs,走 account/bedrock/discover 与 account/bedrock/setup),它挂在一个仍在开发、默认关闭的功能开关 bedrock_setup_wizard 后面,还要求进程内嵌服务、需要登录、提供方仍是默认的 openai 且没有显式配置 model_provider(should_show_bedrock_setup_wizard);上游选服务端时也会因此改用内嵌,因为向导要通过内嵌服务配置提供方。引导期间 Tui 切到 OverlayInput::Onboarding,全屏模式下也不捕获鼠标,好让你用终端自己的选择去复制登录链接和设备码。
目录信任没有放在登录之后立刻问:run_ratatui_app 里的注释写着“Folder consent runs after the picker resolves the actual destination”,要等恢复选择器定下真正要进入的目录,再用同一个引导事件循环跑 check_directory_trust。选择信任后,TUI 用 config/batchWrite 把 projects."<目录>".trust_level 写成 trusted,由 app-server 落盘。
恢复选择器
codex resume、codex fork 不带会话 ID 时,启动流程先跑选择器,得到一个 SessionSelection:
pub enum SessionSelection {
StartFresh,
AgentsOverview,
Resume(SessionTarget),
Fork(SessionTarget),
Exit,
}(codex-rs/tui/src/resume_picker.rs:126)
App::run 按它决定开新线程、恢复、派生还是打开代理总览。列表来自 app-server 的 thread/list,按游标分页,每页 25 条(PAGE_SIZE),选中项离列表末尾不到 5 行(LOAD_NEAR_THRESHOLD)就取下一页,页与页之间按线程去重,因为翻页期间可能有新会话插进来。过滤分两层:提供方、会话来源和工作目录交给服务端筛,你输入的搜索词只在已加载的行里过滤。本地服务的第一页只读状态库里已有的索引(PageLoadMode::StateDbOnly),不从 rollout 文件修补索引。会话中途敲不带参数的 /resume,用的是同一份选择器代码:App 先记下 pending_open_resume_picker,等这次事件处理返回、栈清空之后再打开它。会话存储本身见会话持久化。
代理总览
/agents 与 codex agents 打开代理总览(界面上叫 agent command center),列出后台服务上最近的和本机保留的会话及其子代理,可以筛选、分组、切换、归档、删除,用法见手册子代理。它要求连着共享的后台服务:AppServerTarget 是 Embedded 时,open_agents_overview 只弹出一个“Shared agents unavailable”的选择框,提供“Start background server”和“Return to this session”两个选项。
实现分两块:app/agents_overview*.rs 管数据与动作,app/agent_center/ 管渲染与输入。它本身是一个 BottomPaneView(视图 id 为 agents-overview),显示时 ChatWidget 让出整个画面:
pub(crate) fn as_renderable(&self) -> RenderableItem<'_> {
if self
.bottom_pane
.selected_index_for_active_view(crate::app::AGENTS_OVERVIEW_VIEW_ID)
.is_some()
{
return self.bottom_pane_renderable(
/*footer*/ None,
crate::bottom_pane::CommandPopupPlacement::AboveComposer,
/*composer_gap*/ None,
/*working_tip*/ None,
);
}(codex-rs/tui/src/chatwidget/rendering.rs:129)
App 渲染时再把期望高度设成整个屏幕。数据方面,成员列表在启动和重连后用 thread/loaded/list 与 thread/list 播种,之后的读取只刷新元数据、不会把已有的行踢掉;刷新时对每个会话发 thread/read 取元数据,再用只取一轮的 thread/turns/list 拿最后一条消息。详情面板只用已有的读取结果和已经送达的事件,模块注释强调观察活动既不会附着到线程,也不会额外拉取历史。用量只为选中的那一个任务取,每分钟刷新一次(REFRESH_INTERVAL)。属于另一个 app-server 的任务以冻结的只读快照打开。多 agent 的机制本身见多 agent。
用量面板
/usage 先弹一个底部菜单,两项:View analytics 与 Redeem reset。选 View analytics(或直接敲 /usage daily、weekly、cumulative)会发出 AppEvent::OpenAnalytics:
AppEvent::OpenAnalytics { view: summary_view } => {
tui.enter_alt_screen()?;
let mut view = self.retained_analytics.take().unwrap_or_else(|| {
Box::new(crate::analytics::AnalyticsView::new(self.keymap.list.clone()))
});
// ...
self.overlay = Some(Overlay::Analytics(view));
tui.frame_requester().schedule_frame();(codex-rs/tui/src/app/event_dispatch.rs:1779)
关闭时视图被收进 retained_analytics,下次打开还停在原来的标签页。analytics/ 目录有 30 多个文件,Section 枚举列出八个分区:Usage、Plugins、Credits、Chats、Activity、Skills、Plan、Summary。这里是本篇说的例外之一:账号级的报表不走 app-server,而是由 codex-backend-client 的 AnalyticsSession::from_config 读取本机的 ChatGPT 凭据直接请求后端,缓存按本机账号与用户隔离,与连着哪个服务端无关;对话和任务列表仍经 app-server 的 thread/list 取。所以没用 ChatGPT 账号登录时,/usage 直接报“Sign in with ChatGPT to use /usage.”。
宠物、主题与状态栏
- 宠物(
/pets,别名/pet;pets/):内置宠物是带版本的应用资源,按需下载进CODEX_HOME下的托管缓存;自定义宠物放在$CODEX_HOME/pets/<pet-id>/pet.json。精灵图不经 ratatui,而是在一帧画完之后用终端图像协议另行输出(Tui::draw_ambient_pet_image),协议有Kitty、KittyLocalFile、Sixel三种,Sixel 编码器是自己写的最小实现,用 RGB332 降色。tmux 与 Zellij 里一律禁用(图像跟不住窗格),iTerm2 要 3.6 以上。 - 主题(
/theme;theme_picker.rs):列出内置主题和CODEX_HOME/themes/下的自定义.tmTheme文件;方向键移动时实时换主题预览,取消则恢复打开时的主题,确认才把[tui] theme写进本机config.toml。高亮引擎是syntect加two_face,换主题会递增THEME_REVISION,让已缓存的渲染结果失效。 - 状态栏与终端标题(
/statusline、/title):都是底部视图里的选择器,可以勾选、调序、实时预览,分别存为tui.status_line(默认model-with-reasoning、current-dir、thread-name)和tui.terminal_title(默认activity、thread-name、project-name)。标题用 OSC 写出,写之前剔除控制字符和双向文本格式符,最长 240 个字符(MAX_TERMINAL_TITLE_CHARS),因为标题拼自模型输出、线程名、路径这些不受信的文本。git 分支、PR 号这类项经WorkspaceCommandRunner走 app-server 的command/exec取,工作区在远程时照样准确。
和《从 LLM 到 Coding Agent》对照
那本书的成本与用量追踪一篇强调“实时可见,而非事后算账”,并在客户端里按分价目表把 token 换算成美元。Codex 的 TUI 自己不算钱:token 数随 thread/tokenUsage/updated 通知到达,状态栏可以挂 context-remaining、five-hour-limit、weekly-limit、used-tokens 这些项;企业账号线程的估算花费(estimated-thread-cost)来自服务端返回的 estimated_usage_usd_micros;历史用量则去 /usage 的面板里看后端的报表。“可见性是安全阀”这一点两边一致,只是计价的权威放在了服务端。
上一篇:全屏对话记录 · 搜索、选择与折叠 · 下一篇:exec-server · 在另一台机器上执行