Files
codex-hud/docs/superpowers/specs/2026-03-22-codex-hud-design.md
manpengan befc86acaa docs: record spike 2 results, add spike 1/3 manual scripts
Spike 2 (data source) completed:
- All SQLite threads fields verified stable (model, cwd, git_branch, tokens_used)
- JSONL event names stable across 2 sessions (function_call, task_started, token_count, etc.)
- Bonus: token_count event has rate_limits.primary/secondary.used_percent and plan_type
- HudState updated with rateLimitPrimary, rateLimitSecondary, planType fields

Spike 1 (pane mode): pending - tmux not installed, manual steps documented
Spike 3 (inline): pending - needs real TTY, test script provided

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 09:05:37 +08:00

23 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 JSONL token_count event (bonus: rate limit data)
  rateLimitPrimary: { usedPercent: number; windowMinutes: number; resetsAt: number } | null
  rateLimitSecondary: { usedPercent: number; windowMinutes: number; resetsAt: number } | null
  planType: string | null   // "plus" | "pro" | "team" | ...

  // 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. Pre-Implementation Spikes

这三个 spike 必须在写 implementation plan 之前完成。每个 spike 是独立的小脚本或手工验证,目标是锁定架构假设,不是实现功能。

Spike 状态总览2026-03-22

Spike 状态 结论
Spike 1: Pane Mode Capability 待手工验证 tmux 未安装,需先安装再测
Spike 2: Data Source Stability 已完成 SQLite + JSONL 均稳定可依赖,有额外 rate limit 发现
Spike 3: Inline Fallback Safety 待手工验证 需在真实 TTY 中运行测试脚本

Spike 1: Pane Mode Capability

目标: 确认 tmux/zellij 下可靠分屏、在主 pane 跑真实 codex、退出后自动清理 HUD pane。

验证方法:

# 在 tmux session 内运行:
# 1. 用 display-message 探测 session 可操作性
tmux display-message -p '#{session_id}'

# 2. split 出 4 行底部 pane在里面跑 watch 模拟 HUD
tmux split-window -v -l 4 'watch -n 0.5 date'

# 3. 在主 pane 跑真实 codex或任意 full-screen TUI如 vim
codex

# 4. 退出 codex验证 HUD pane 是否自动关闭
# 用 tmux kill-pane -t <pane_id> 从代码层面关闭

通过标准:

  • display-message 在正常 session 下返回有效 session_id在嵌套/只读 session 下可检测失败
  • split pane 成功HUD pane 内容独立于主 pane主 pane codex TUI 不受干扰
  • codex 退出后,tmux kill-pane 能可靠关闭 HUD pane不留残留

失败后的降级决策:

  • 探测不可靠 → pane mode 进入条件收紧,宁可误降级到 inline 也不误进 pane
  • kill-pane 不可靠 → pane 生命周期改为"codex 退出时发送信号给 HUD 子进程HUD 进程自行退出"

当前状态2026-03-22tmux 未安装,待手工验证。 手工步骤:brew install tmux → 在 tmux session 内运行上述验证命令 → 记录结论。


Spike 2: Data Source Stability

目标: 确认 state_5.sqlitesessions/*.jsonl 哪些字段在活动会话里稳定更新,哪些不可依赖。

验证方法:

# 1. 跑一个真实 codex session同时在另一窗口执行

# SQLite每 2 秒看 threads 表最新行
watch -n 2 'sqlite3 ~/.codex/state_5.sqlite \
  "SELECT model, cwd, git_branch, tokens_used, updated_at \
   FROM threads ORDER BY updated_at DESC LIMIT 3"'

# SQLite看 jobs/agent_jobs 表是否有更新
sqlite3 ~/.codex/state_5.sqlite ".tables"
sqlite3 ~/.codex/state_5.sqlite "SELECT * FROM jobs LIMIT 5"

# JSONLtail 最新 session 文件,观察 type 字段分布
tail -f ~/.codex/sessions/$(date +%Y/%m/%d)/rollout-*.jsonl | \
  python3 -c 'import sys,json; [print(json.loads(l).get("type","?")) for l in sys.stdin]'

通过标准(按字段逐一确认):

字段 预期来源 通过标准
threads.model SQLite session 开始时写入,不频繁变化
threads.cwd SQLite 准确反映当前工作目录
threads.git_branch SQLite git branch --show-current 一致
threads.tokens_used SQLite 随对话进行递增
threads.updated_at SQLite 每轮对话后更新
JSONL type 字段 JSONL 每个事件都有 type且值稳定可枚举
JSONL tool/task 事件 JSONL 至少能识别 tool 调用开始/结束、task 进度

失败后的降级决策:

  • tokens_used 不更新 → HUD 不显示 tokens标 n/a
  • JSONL 事件名不稳定 → normalize 层对所有未知 type 返回 nullHUD 对应字段降为 n/a
  • git_branch 不准确 → 改为本地 git branch --show-current 补充

Spike 2 结论已完成2026-03-22

SQLite threads 表 — 全部字段已验证稳定:

字段 实测值样本 结论
model gpt-5.4 稳定session 开始时写入
cwd /Users/manpengan/pro/nas-infra 稳定,准确
git_branch main / 空字符串(非 git 目录) 稳定,非 git 目录为空字符串
tokens_used 710865, 6413335 稳定递增,线程累计值
updated_at Unix timestamp 每轮对话更新

JSONL 事件 — 跨 2 个 session 验证,事件名稳定:

event 类型 payload.type 关键字段 HUD 用途
session_meta cwd, cli_version, model_provider session 初始化
event_msg task_started turn_id, model_context_window: 258400 turn 开始context window 大小
event_msg task_complete turn_id, last_agent_message turn 结束
event_msg token_count rate_limits.primary.used_percent, secondary.used_percent, plan_type rate limit 显示
event_msg agent_message message, phase agent 回复内容
response_item function_call name(工具名), call_id tool 开始
response_item function_call_output call_id(匹配) tool 结束
response_item reasoning 内部推理 可忽略
turn_context cwd, timezone, approval_policy 每轮上下文

额外发现(超出原预期):

  • token_count.rate_limits 直接给出 primary.used_percent5分钟窗口secondary.used_percent(周窗口)
  • 这是 claude-hud 需要单独调用 OAuth API 才能拿到的数据Codex 直接写在 JSONL 里
  • model_context_window: 258400 给出每个 turn 的 context 窗口大小

结论对 HudState 的影响:

  • tokensUsedTotal:来自 SQLite threads.tokens_used,可信
  • rateLimitPrimary / rateLimitSecondary:来自 token_count JSONL 事件,可信(新增字段
  • activeTool:来自 function_call / function_call_output 配对,可信
  • taskProgress:无直接字段,需从 task_started/task_complete 事件计数推断
  • context window usagemodel_context_window 大小已知,但当前 turn 的 input token 数暂未找到来源

更新后的 HudState在 Section 4.2 中同步):

// 新增字段(来自 token_count 事件)
rateLimitPrimary: { usedPercent: number; windowMinutes: number; resetsAt: number } | null
rateLimitSecondary: { usedPercent: number; windowMinutes: number; resetsAt: number } | null
planType: string | null  // "plus" | "pro" | ...

Spike 3: Inline Fallback Safety

目标: 确认 codex --no-alt-screen 下能预留底部区域,且 wrapper crash/exit 后不会弄脏终端。

验证方法:

# 1. 确认 --no-alt-screen 模式下 codex 以滚动方式输出
codex --no-alt-screen

# 2. 用小脚本模拟 scroll region + 底部 HUD然后正常退出和异常退出各跑一次
node -e "
process.stdout.write('\x1b[s');               // save cursor
process.stdout.write('\x1b[1;' + (process.stdout.rows - 4) + 'r');  // scroll region
process.stdout.write('\x1b[' + (process.stdout.rows - 3) + ';1H');  // move to HUD area
process.stdout.write('=== HUD TEST LINE ===');
process.stdout.write('\x1b[u');               // restore cursor
setTimeout(() => {
  // cleanup
  process.stdout.write('\x1b[r');             // reset scroll region
  process.stdout.write('\x1b[' + process.stdout.rows + ';1H\x1b[2K'); // clear HUD
  console.log('cleaned up');
}, 3000);
"

# 3. 验证 Ctrl-C 后终端是否正常(光标可见,滚动恢复)
# 4. 在不同终端宽度下测试80, 120, 200 列)

通过标准:

  • scroll region 设置后,主内容区域可滚动,底部 4 行固定不动
  • 正常退出后终端恢复原始状态光标可见scroll region 清除)
  • Ctrl-C / kill 后终端不残留 HUD 内容,不留隐藏光标
  • macOS Terminal.app + iTerm2 + Ghostty 下行为一致(至少两种实测)

失败后的降级决策:

  • scroll region 在任一目标终端不稳定 → inline mode 标记为"实验性",默认不启用,需 --inline flag 显式开启
  • cleanup 不可靠 → cleanup 逻辑改为双重保险:正常路径 + process.on('exit') 兜底

当前状态2026-03-22需在真实 TTY 中手工验证。 在真实终端(非 Claude Code 子 shell中运行

# 保存到 /tmp/spike3.mjs 后执行node /tmp/spike3.mjs
const rows = process.stdout.rows;
const cols = process.stdout.columns;
if (!rows) { console.error('Not a TTY'); process.exit(1); }
console.log(`Terminal: ${cols}x${rows}`);
process.stdout.write('\x1b[s');
process.stdout.write(`\x1b[1;${rows - 4}r`);
process.stdout.write(`\x1b[${rows - 3};1H\x1b[2K=== HUD 1: model | cwd | main* | 2m ===`);
process.stdout.write(`\x1b[${rows - 2};1H\x1b[2K=== HUD 2: tokens 12,340 | Write  3s ===`);
process.stdout.write(`\x1b[${rows - 1};1H\x1b[2K=== HUD 3: shell(3) read(7) ===`);
process.stdout.write(`\x1b[${rows};1H\x1b[2K=== HUD 4: task 3/5 ===`);
process.stdout.write('\x1b[u');
setTimeout(() => {
  process.stdout.write('\x1b[r');
  for (let i = rows - 3; i <= rows; i++)
    process.stdout.write(`\x1b[${i};1H\x1b[2K`);
  process.stdout.write(`\x1b[${rows};1H`);
  console.log('\nCLEANUP OK');
}, 3000);

通过标准:

  • HUD 4 行固定在底部,主区域可自由滚动
  • 3 秒后自动清除,光标恢复,终端无残留
  • 在 macOS Terminal.app / iTerm2 / Ghostty 中各测一次

三个 spike 全部通过后,才进入 implementation plan。 每个 spike 的结论(通过 / 部分通过 + 条件 / 失败 + 降级)应记录在 implementation plan 的前置说明中。

11. 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 为首选目标