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

395 lines
14 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 (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
```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` 启发式推断 — 删除
**渲染层共享边界:**
- `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
```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. 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 为首选目标