- Add confidence/risk assessment section (confirmed, unverified, risks) - Explicitly frame inline mode as degraded fallback, not peer to pane - Clarify renderer shared boundary (layout output only, not terminal control) - Add SQLite schema versioning and JSONL protocol instability mitigations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
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 (degraded fallback, NOT peer to pane mode)
定位:inline mode 是无 pane 能力时的退化方案,不是与 pane mode 平级的体验模式。 它需要接管 scroll region 和终端清理,风险显著高于 pane mode。 开发和测试优先级应低于 pane mode。
| 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 体验)
--no-alt-screen不应自动强推;仅在 pane 能力探测失败时才进入此模式
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
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<string, number>
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启发式推断 — 删除
渲染层共享边界:
pane-renderer和inline-renderer只共享layout.ts输出的string[](纯文本行)- 不共享任何底层终端控制逻辑(scroll region、cursor、PTY 管道)
- 两者的终端假设完全不同,不应试图抽象出公共终端控制层
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
codex-hud install
执行:
- 写 wrapper 到
~/.codex-hud/bin/codex(薄 shell 脚本 -> 固定绝对路径dist/cli.js) - 在 shell rc 文件中用标记块前置 PATH:
# >>> codex-hud >>> export PATH="$HOME/.codex-hud/bin:$PATH" # <<< codex-hud <<< - 写 manifest.json
7.2 Manifest
{
"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
CODEX_REAL_BIN环境变量(显式 override)manifest.json记录的路径- PATH 中排除
~/.codex-hud/bin后的第一个codex
7.4 Bypass
codex --no-hud [args...]
直接 execvp 真实 binary,wrapper 进程替换,零开销。
7.5 Uninstall
codex-hud uninstall
- 删除
~/.codex-hud/bin/codex - 删除 shell rc 中的标记块
- 删除
manifest.json - 保留
config.json(用户数据)
7.6 Doctor
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. Confidence & Risk Assessment
10.1 已确认(可直接依赖)
| 项 | 依据 |
|---|---|
~/.codex/state_5.sqlite 可读 |
本地实测,threads 表含 model/cwd/git_branch/tokens_used |
~/.codex/sessions/*.jsonl 存在 |
本地实测,含 session_meta/event_msg/response_item/task_started 等 type |
Codex 支持 --no-alt-screen |
codex --help 已确认 |
tmux split-window 分屏可行 |
标准 tmux 功能 |
10.2 待验证(normalize 层隔离,不写死)
| 项 | 风险 | 对策 |
|---|---|---|
| JSONL 事件字段名(task_started 等) | 可能随 Codex 版本变化 | normalize(raw) -> HudEvent | null,字段名只在 normalize 内部出现 |
| SQLite 表结构(state_5.sqlite) | 表名含版本号,可能升级 | 启动时检测 state_*.sqlite 最新版本;schema 读取失败降级为 n/a |
threads.tokens_used 精确语义 |
可能是 prompt tokens / completion tokens / 合计 | 命名为 tokensUsedTotal,UI 只展示原始数值,不做百分比推算 |
| context window usage | 当前未确认有原生来源 | 默认 n/a,不伪装 |
10.3 明确风险
| 风险 | 影响 | 缓解 |
|---|---|---|
| pane 生命周期管理 | 嵌套 session、退出时未关闭 pane、split 失败 | 双重探测进入条件;exit handler 强制关闭 pane |
| inline scroll region | 光标恢复失败、终端兼容性差异、resize 后撕裂 | 定位为 degraded fallback,开发优先级低于 pane mode;cleanup 必须覆盖所有退出路径 |
| JSONL 事件协议不稳定 | Codex 升级后解析失败 | normalize 层隔离,失败静默降级,HUD 其他部分不受影响 |
| SQLite schema 升级 | state_5 变成 state_6 | glob 匹配最新 state_*.sqlite,schema 不匹配时降级 |
| tokensUsedTotal 不等于 context | 用户可能误读为"剩余空间" | UI 只写 "tokens" 不写 "context",不画百分比条 |
11. Tech Stack
- Node.js 20+ / TypeScript
node-pty(inline mode PTY)better-sqlite3(SQLite 读取)- macOS + zsh 为首选目标