# 其它界面 · 登录引导、恢复选择、代理总览与用量面板

> 聊天界面之外，TUI 还有一圈界面：登录引导与目录信任、恢复选择器、代理总览、用量面板、终端宠物、主题与状态栏。它们按挂载方式分成三类：App::run 之前自带事件循环的启动界面，ChatWidget 底部视图栈里的弹窗，以及 App 在备用屏幕上开的覆盖层。本篇逐个点到，重点讲清它们挂在哪、数据从哪来。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/tui-surfaces
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 其它界面 · 登录引导、恢复选择、代理总览与用量面板

前几篇围绕聊天主界面：[事件循环](https://daiw.net/manual/codex-source/tui-architecture)、[输入框](https://daiw.net/manual/codex-source/chat-composer)、[历史记录单元](https://daiw.net/manual/codex-source/history-cells)和[全屏对话记录](https://daiw.net/manual/codex-source/fullscreen-transcript)。`codex-tui` 里还有不少别的界面，每个都有自己的状态机，这一篇不深挖其中任何一个，只回答两个问题：它挂在哪，数据从哪来。

## 三种挂法

```mermaid
flowchart TB
  subgraph PRE[App::run 之前]
    ON[登录引导<br/>欢迎、登录、Bedrock]
    RP[恢复或派生选择器]
    TR[目录信任确认]
  end
  subgraph LOOP[App 事件循环]
    CW[ChatWidget] --> BP[BottomPane 视图栈<br/>代理总览、主题、状态栏、宠物、用量菜单]
    CW --> SS[状态栏与终端标题]
    OV[App.overlay 备用屏幕<br/>用量面板、对话记录、静态分页]
  end
  PRE --> LOOP
  LOOP -.->|resume 命令| RP
```

- **启动界面**：在 `App::run` 之前运行，从启动流程手里借用 `Tui`，自己跑一个小的 `select!` 事件循环，结束后把结果交回启动流程。登录引导、会话选择器、目录信任确认都属于这一类。
- **底部视图**：实现 `BottomPaneView` trait，压进 `ChatWidget` 底部的视图栈（见[输入框](https://daiw.net/manual/codex-source/chat-composer)一篇），不算测试有近 20 个实现，从选择列表、审批框到代理总览都是。
- **覆盖层**：`App` 的 `overlay` 字段，类型 `Overlay`，三个变体 `Transcript`、`Static`、`Analytics`，打开时进入备用屏幕，关闭时回到原来的界面。

和前几篇一样，这些界面也尽量经 app-server 取数、写数，只有少数例外，下文逐个注明。

## 登录引导与目录信任

`onboarding/` 是一个很小的步骤状态机，`Step` 只有 `Welcome`、`Auth`、`TrustDirectory` 三种。要不要登录，判定只有两个条件：

```rust
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`：

```rust
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`，等这次事件处理返回、栈清空之后再打开它。会话存储本身见[会话持久化](https://daiw.net/manual/codex-source/rollout-and-storage)。

## 代理总览

`/agents` 与 `codex agents` 打开代理总览（界面上叫 agent command center），列出后台服务上最近的和本机保留的会话及其子代理，可以筛选、分组、切换、归档、删除，用法见手册[子代理](https://daiw.net/manual/codex/subagents)。它要求连着共享的后台服务：`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` 让出整个画面：

```rust
    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](https://daiw.net/manual/codex-source/multi-agent)。

## 用量面板

`/usage` 先弹一个底部菜单，两项：View analytics 与 Redeem reset。选 View analytics（或直接敲 `/usage daily`、`weekly`、`cumulative`）会发出 `AppEvent::OpenAnalytics`：

```rust
            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》对照

那本书的[成本与用量追踪](https://daiw.net/manual/llm-to-agent/cost-tracking)一篇强调“实时可见，而非事后算账”，并在客户端里按分价目表把 token 换算成美元。Codex 的 TUI 自己不算钱：token 数随 `thread/tokenUsage/updated` 通知到达，状态栏可以挂 `context-remaining`、`five-hour-limit`、`weekly-limit`、`used-tokens` 这些项；企业账号线程的估算花费（`estimated-thread-cost`）来自服务端返回的 `estimated_usage_usd_micros`；历史用量则去 `/usage` 的面板里看后端的报表。“可见性是安全阀”这一点两边一致，只是计价的权威放在了服务端。

---

上一篇：[全屏对话记录 · 搜索、选择与折叠](https://daiw.net/manual/codex-source/fullscreen-transcript) · 下一篇：[exec-server · 在另一台机器上执行](https://daiw.net/manual/codex-source/exec-server)
