Files
codex-hud/docs/superpowers/specs/2026-03-22-codex-hud-design.md
manpengan e38e56ba3e docs: add codex-hud design spec and original plan
Design spec covers dual-mode architecture (tmux pane + inline fallback),
SQLite/JSONL data layer, HUD layout, installation, and error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 08:48:25 +08:00

355 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (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<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` 启发式推断 — 删除
## 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` 真实 binarywrapper 进程替换,零开销。
### 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 为首选目标