# Codex HUD — Design Spec > Date: 2026-03-22 > Status: Approved > Author: manpengan + Claude --- ## 1. Problem Codex CLI(OpenAI)运行时没有实时状态面板。用户看不到当前 session 的 token 消耗、tool 活动、task 进度等信息,只能在 TUI 内滚动查看零散输出。 claude-hud 为 Claude Code 解决了这个问题,但它依赖 Claude Code 的原生 statusline plugin API。Codex 没有这个 API。 ## 2. Key Discovery Codex 在本地写入了结构化数据,足以支撑 HUD: | 数据源 | 路径 | 内容 | |--------|------|------| | SQLite | `~/.codex/state_5.sqlite` | threads, jobs, agent_jobs, agent_job_items, logs 等表 | | Session JSONL | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | 结构化事件(session_meta, event_msg, response_item, task_started) | | History | `~/.codex/history.jsonl` | 全局历史 | | TUI log | `~/.codex/log/codex-tui.log` | TUI 日志 | 其中 `threads` 表直接包含:`rollout_path`, `cwd`, `model`, `tokens_used`, `git_branch`, `updated_at`。 **结论:主数据源放在 SQLite + Session JSONL,不走 stdout 启发式解析。** ## 3. Architecture ### 3.1 Mode Selection 启动时按优先级降序选择渲染模式: ``` codex-hud 启动 | 探测 pane 能力(不只看 $TMUX/$ZELLIJ 环境变量) |-- tmux: `display-message` 验证 client/session 可操作 --> pane mode |-- zellij: `action` 能力探测 --> pane mode |-- 探测失败 --> inline mode (inject --no-alt-screen) |-- inline 也失败(TTY 不可用 / 渲染初始化失败 / 被禁用)--> passthrough mode(纯透传,无 HUD) ``` 探测逻辑: - 有环境变量不代表可安全 split(可能是嵌套 session、只读 client、shell 残留) - tmux: 执行 `tmux display-message -p '#{session_id}'` 验证可操作性 - zellij: 执行对应 action 探测 - 探测失败才降级 ### 3.2 Pane Mode (preferred) ``` +--------------------------------------+ | Codex full-screen TUI (main) | | 原生体验,参数不做任何修改 | +--------------------------------------+ | claude-4o | ~/pro | main* | 2m | <-- HUD pane (4 lines) | tokens 12,340 | Write 12s | 纯 SQLite 轮询 | shell(3) read(7) write(2) | 无 PTY | task 3/5 | rollout-2026-03-22 | +--------------------------------------+ ``` - `tmux split-window -l 4` 开底部 pane - HUD 作为 wrapper 内子模块运行在 pane 中(不是独立进程) - 主 pane 启动真实 codex,参数原样透传 - Codex 退出时自动关闭 HUD pane ### 3.3 Inline Mode (fallback) ``` | Codex 滚动输出 (--no-alt-screen) | | > Read file: src/index.ts | +--------------------------------------+ | claude-4o | ~/pro | main* | 2m | <-- ANSI scroll region | tokens 12,340 | Write 12s | PTY wrapper 维护 | shell(3) read(7) write(2) | | task 3/5 | rollout-2026-03-22 | +--------------------------------------+ ``` - PTY wrapper 注入 `--no-alt-screen`,预留底部 4 行 - ANSI scroll region + cursor save/restore - Codex 以滚动模式运行(牺牲全屏 TUI 体验) ### 3.4 Passthrough Mode (last resort) - `child_process.spawn` 真实 codex,完全透传 - wrapper 退出码跟随 codex - 触发条件:不在 tmux/zellij + inline 注入失败/被显式禁用 + TTY 不可用 + 渲染初始化失败 ## 4. Data Layer 两种渲染模式共用同一套数据层。 ### 4.1 Observers ``` sqlite observer -- 500-1000ms 低频轮询 --> 稳定状态 (model, tokens, branch, jobs) jsonl observer -- fs.watch 唤醒 + offset 增量 tail + 低频轮询兜底 --> 实时事件 | state/store.ts 收到事件 --> 立即重绘 空闲时 --> 1s timer 兜底刷新(elapsed 计时用) ``` **SQLite observer:** - 每 500-1000ms 轮询 `~/.codex/state_5.sqlite` - 读取最新 `threads` 行:model, cwd, git_branch, tokens_used, updated_at - 可选读 `jobs`, `agent_jobs` 表 **JSONL observer:** - `fs.watch` 只负责唤醒(macOS 会丢边缘事件) - 真正读取靠"记住 offset 后增量 tail" - 额外加低频轮询兜底(防止 fs.watch 丢事件) - 事件标准化:`normalize(rawEvent) -> HudEvent | null` - 不硬编码事件字段名,先 normalize 再处理 - 无法识别的事件返回 null,静默跳过 ### 4.2 HudState ```typescript type HudState = { // from SQLite threads table model: string | null cwd: string gitBranch: string | null tokensUsedTotal: number | null // thread 累计 tokens_used,不是 context 窗口占用率 sessionPath: string | null // from JSONL events (via normalize) activeTool: { name: string; startedAt: number } | null toolCounts: Record taskProgress: { done: number; total: number } | null // from timer elapsedSec: number // render meta renderMode: RenderMode // "pane" | "inline" | "passthrough" termWidth: number termHeight: number } type RenderMode = "pane" | "inline" | "passthrough" type HudEvent = | { type: "task_progress"; done: number; total: number } | { type: "tool_start"; tool: string } | { type: "tool_end"; tool: string } | { type: "session_meta"; model: string } // ... normalize layer handles mapping raw event names to these ``` **语义约束:** - `tokensUsedTotal` 是累计消耗值,不是 context window 占用百分比,不渲染成百分比条 - `activeTool` 带时间戳,用于显示持续时长和超时回收 - 无法可靠获取的字段显示 `n/a` 或 `~estimated` ## 5. Components & File Structure ``` src/ cli.ts # 入口:解析 argv,运行模式检测,路由 launcher/ resolve-codex.ts # 找真实 codex binary argv.ts # passthrough args, --no-hud, inline 模式注入 --no-alt-screen pane.ts # tmux/zellij pane 生命周期 (open, resize, close) detect/ tmux.ts # display-message 探测,返回 { capable, sessionId } zellij.ts # action 探测,返回 { capable } observers/ sqlite.ts # 轮询 state_5.sqlite,产出 SqliteSnapshot jsonl.ts # tail 最新 session JSONL,产出 HudEvent stream state/ store.ts # 合并 SqliteSnapshot + HudEvent -> HudState, emit change timer.ts # session elapsed (wrapper 启动时间起算) terminal/ pane-renderer.ts # pane 模式:clearScreen 循环,写入 stdout inline-renderer.ts # inline 模式:ANSI scroll region + cursor save/restore layout.ts # dense(4) / compact(3) 布局,输入 HudState 输出 string[] tty.ts # resize 监听,cleanup (scroll region 还原, cursor 还原) types/ hud.ts # HudState, HudEvent, SqliteSnapshot, RenderMode config.ts # ~/.codex-hud/config.json, defaults install.ts # install / uninstall / doctor ``` **相比原计划删除的部分:** - `observers/stdout-parser.ts` — 删除(不做 stdout 启发式解析) - `observers/app-server.ts` — 删除 - `observers/process-tree.ts` — 删除 - `state/tools.ts` / `state/agents.ts` 启发式推断 — 删除 ## 6. HUD Layout ### 6.1 Dense Mode (4 lines, terminal height >= 24) ``` Line 1: {model} | {cwd_short} | {branch}{dirty} | {elapsed} Line 2: tokens {tokensUsedTotal} | {activeTool.name} {duration} Line 3: {toolCounts summary} Line 4: task {done}/{total} | {sessionLabel} ``` ### 6.2 Compact Mode (3 lines, terminal height < 24) ``` Line 1: {model} | {branch} | {elapsed} Line 2: tokens {tokensUsedTotal} | {activeTool.name} Line 3: {toolCounts compact} | task {done}/{total} ``` ### 6.3 Width Degradation | Width | 丢弃 | |-------|------| | < 100 | sessionLabel | | < 80 | toolDuration, dirty 标记 | | < 60 | toolCounts,只保留 activeTool | | < 40 | 只保留 model + elapsed | ### 6.4 Field Provenance - 来自 SQLite / JSONL normalize(可信): 正常白色 - 推断/估算: 暗色 + `~` 前缀 - 不可用: `n/a`(暗色) ## 7. Installation ### 7.1 Install ```bash codex-hud install ``` 执行: 1. 写 wrapper 到 `~/.codex-hud/bin/codex`(薄 shell 脚本 -> 固定绝对路径 `dist/cli.js`) 2. 在 shell rc 文件中用标记块前置 PATH: ```bash # >>> codex-hud >>> export PATH="$HOME/.codex-hud/bin:$PATH" # <<< codex-hud <<< ``` 3. 写 manifest.json ### 7.2 Manifest ```json { "realBin": "/usr/local/bin/codex", "wrapperPath": "~/.codex-hud/bin/codex", "installedAt": "2026-03-22T...", "version": "0.1.0", "shellRcFilesModified": ["~/.zshrc"], "pathEntry": "$HOME/.codex-hud/bin" } ``` ### 7.3 Real Binary Resolution 1. `CODEX_REAL_BIN` 环境变量(显式 override) 2. `manifest.json` 记录的路径 3. PATH 中排除 `~/.codex-hud/bin` 后的第一个 `codex` ### 7.4 Bypass ```bash codex --no-hud [args...] ``` 直接 `execvp` 真实 binary,wrapper 进程替换,零开销。 ### 7.5 Uninstall ```bash codex-hud uninstall ``` - 删除 `~/.codex-hud/bin/codex` - 删除 shell rc 中的标记块 - 删除 `manifest.json` - 保留 `config.json`(用户数据) ### 7.6 Doctor ```bash codex-hud doctor ``` 检查: - real binary 可达 - wrapper 在 PATH 中优先 - `codex --no-hud --help` 真实可执行 - `~/.codex/state_5.sqlite` 可读 - tmux / zellij 能力检测 - config 可写 ## 8. Error Handling & Degradation ### 8.1 Observer Failure | 场景 | 行为 | |------|------| | SQLite 不可读 | tokensUsedTotal: null,显示 n/a,每 5s 重试 | | 最新 JSONL 找不到 | toolCounts 清空,activeTool: null,不报错 | | JSONL normalize 返回 null | 静默跳过,不更新 state | | fs.watch 失败 | 降回纯轮询 (1s),不影响 HUD 显示 | ### 8.2 Render Failure Cascade ``` pane 初始化失败 --> 自动降级到 inline mode inline 初始化失败 --> 自动降级到 passthrough mode 任何模式崩溃 --> tty.ts cleanup --> 退出 ``` ### 8.3 Process Exit Cleanup (mandatory, all paths) ``` 1. inline mode: 还原 scroll region,清除 HUD 行,显示光标 2. pane mode: 关闭 HUD pane 3. PTY: 等待子进程退出或超时 kill 4. 退出码跟随真实 codex ``` 处理 `exit`, `SIGINT`, `SIGTERM`, `uncaughtException`。任何 crash 先跑 cleanup,再打印简短错误,以真实 codex 退出码退出。绝不让用户终端停在残留 scroll region 或隐藏光标状态。 ## 9. Product Rules - pane 模式不修改传给 codex 的任何参数 - inline 模式才注入 `--no-alt-screen` - `--no-alt-screen` 不应自动强推,它是 fallback 而非平级体验 - 无法可靠获取的指标显示 `n/a` 或 `~estimated`,绝不虚构精度 - wrapper 必须支持 `codex --no-hud` 立即 bypass - crash 后必须恢复 terminal 状态 - install 必须可逆(one command uninstall) - shell rc 修改使用标记块,安装幂等 ## 10. Tech Stack - Node.js 20+ / TypeScript - `node-pty` (inline mode PTY) - `better-sqlite3` (SQLite 读取) - macOS + zsh 为首选目标