Files
codex-hud/docs/superpowers/specs/2026-03-22-codex-hud-design.md
manpengan 9176bd9c1b docs: tighten design spec with risk assessment and mode hierarchy
- 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>
2026-03-22 08:56:03 +08:00

14 KiB
Raw Blame History

Codex HUD — Design Spec

Date: 2026-03-22 Status: Approved Author: manpengan + Claude


1. Problem

Codex CLIOpenAI运行时没有实时状态面板。用户看不到当前 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
  • 读取最新 threadsmodel, 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-rendererinline-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

执行:

  1. 写 wrapper 到 ~/.codex-hud/bin/codex(薄 shell 脚本 -> 固定绝对路径 dist/cli.js
  2. 在 shell rc 文件中用标记块前置 PATH
    # >>> codex-hud >>>
    export PATH="$HOME/.codex-hud/bin:$PATH"
    # <<< codex-hud <<<
    
  3. 写 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

  1. CODEX_REAL_BIN 环境变量(显式 override
  2. manifest.json 记录的路径
  3. PATH 中排除 ~/.codex-hud/bin 后的第一个 codex

7.4 Bypass

codex --no-hud [args...]

直接 execvp 真实 binarywrapper 进程替换,零开销。

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 / 合计 命名为 tokensUsedTotalUI 只展示原始数值,不做百分比推算
context window usage 当前未确认有原生来源 默认 n/a不伪装

10.3 明确风险

风险 影响 缓解
pane 生命周期管理 嵌套 session、退出时未关闭 pane、split 失败 双重探测进入条件exit handler 强制关闭 pane
inline scroll region 光标恢复失败、终端兼容性差异、resize 后撕裂 定位为 degraded fallback开发优先级低于 pane modecleanup 必须覆盖所有退出路径
JSONL 事件协议不稳定 Codex 升级后解析失败 normalize 层隔离失败静默降级HUD 其他部分不受影响
SQLite schema 升级 state_5 变成 state_6 glob 匹配最新 state_*.sqliteschema 不匹配时降级
tokensUsedTotal 不等于 context 用户可能误读为"剩余空间" UI 只写 "tokens" 不写 "context",不画百分比条

11. Tech Stack

  • Node.js 20+ / TypeScript
  • node-pty (inline mode PTY)
  • better-sqlite3 (SQLite 读取)
  • macOS + zsh 为首选目标