# Codex HUD — Design Spec > Date: 2026-03-22 > Status: Spike Gate PASSED — Ready for Implementation Plan > 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 ```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 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-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` 真实 binary,wrapper 进程替换,零开销。 ### 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. Pre-Implementation Spikes > 这三个 spike 必须在写 implementation plan 之前完成。每个 spike 是独立的小脚本或手工验证,目标是锁定架构假设,不是实现功能。 **Spike 状态总览(2026-03-22):** | Spike | 状态 | 结论 | |-------|------|------| | Spike 1: Pane Mode Capability | **已完成** | **PASS** — pane split/cleanup/probe 全部验证通过 | | Spike 2: Data Source Stability | **已完成** | **PASS** — SQLite + JSONL 均稳定可依赖,有额外 rate limit 发现 | | Spike 3: Inline Fallback Safety | **已完成** | **PASS(条件)** — scroll region + cleanup 可行,但限于 tmux 内 TTY;浏览器终端/非 TTY 不可用 | --- ### Spike 1: Pane Mode Capability **目标:** 确认 tmux/zellij 下可靠分屏、在主 pane 跑真实 codex、退出后自动清理 HUD pane。 **验证方法:** ```bash # 在 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 从代码层面关闭 ``` **通过标准:** - `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 进程自行退出" ### Spike 1 结论(已完成,2026-03-22) **测试环境:** tmux 3.6a,macOS,200x50 terminal | 验证项 | 结果 | 细节 | |--------|------|------| | 双重探测逻辑 | PASS | `$TMUX` 非空且 `display-message -p '#{session_id}'` 输出非空 = in tmux;外部调用两者均失败,正确降级 | | `$TMUX` 非空判断 | PASS | 在 session 内 `$TMUX=/private/tmp/tmux-501/default,53544,0`;外部为空字符串 | | `display-message` 探测 | **注意** | exit code 对无效 session 仍返回 0;必须检查 stdout 是否非空,不能依赖 exit code | | split-window -v -l 4 | PASS | 主 pane 200x50 → 200x45;HUD pane 200x4,几何正确 | | pane 独立性 | PASS | 主 pane 输出不干扰 HUD pane,HUD pane 内容稳定 | | kill-pane cleanup | PASS | `kill-pane -t $HUD_PANE` 成功,主 pane 自动恢复 200x50 | **关键实现约束(从结论提炼):** - 探测必须同时检查 `[ -n "$TMUX" ]` AND `display-message stdout 非空`,不能只看 exit code - `split-window` 要用 `-P -F '#{pane_id}'` 获取 pane id,cleanup 才能精确 kill - pane mode gate 可信,可作为主路径实现 --- ### Spike 2: Data Source Stability **目标:** 确认 `state_5.sqlite` 和 `sessions/*.jsonl` 哪些字段在活动会话里稳定更新,哪些不可依赖。 **验证方法:** ```bash # 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" # JSONL:tail 最新 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 返回 null,HUD 对应字段降为 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_percent`(5分钟窗口)和 `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 usage:`model_context_window` 大小已知,但当前 turn 的 input token 数暂未找到来源 **更新后的 HudState(在 Section 4.2 中同步):** ```typescript // 新增字段(来自 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 后不会弄脏终端。 **验证方法:** ```bash # 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')` 兜底 ### Spike 3 结论(已完成,2026-03-22) **测试环境:** Node.js,tmux session(真实 TTY),200x50 | 验证项 | 结果 | 细节 | |--------|------|------| | scroll region 设置 | PASS | `\x1b[1;${rows-4}r` 正确预留底部 4 行,主区域可正常滚动 | | HUD 行固定 | PASS | 10 行内容滚动期间底部 4 行 HUD 保持不动 | | 正常退出 cleanup | PASS | `\x1b[r` 还原 scroll region + 逐行 `\x1b[2K` 清除,终端恢复干净 | | SIGINT cleanup | PASS | `process.on('SIGINT')` + `process.on('exit')` 双保险均触发 cleanup | | codex --no-alt-screen | PASS | 无 `\x1b[?1049h` 序列,确认不进 alternate screen | | 非 TTY 环境 | 已知限制 | `process.stdout.rows` 为 undefined,必须在启动时检查 isTTY | **关键实现约束(从结论提炼):** - 启动时必须检查 `process.stdout.isTTY`,非 TTY 直接降级到 passthrough - cleanup 必须注册 `exit` + `SIGINT` + `SIGTERM` + `uncaughtException` 四个钩子 - scroll region 还原后需逐行 clear,不能只 reset region(会留残影) - inline mode 在 tmux session 内 TTY 可用,可作为 fallback 保留 --- --- ### 三个 Spike 综合结论(2026-03-22) **所有 gate 已过,可进入 implementation plan。** | 架构决策 | 结论 | |----------|------| | pane mode 为主路径 | **确认** — split/probe/cleanup 全部可靠 | | inline mode 为 fallback | **确认保留** — scroll region + cleanup 可行,需 isTTY 检查 | | passthrough 为最终兜底 | **确认** — 非 TTY / 探测全失败时使用 | | SQLite 为主数据源 | **确认** — 所有字段稳定 | | JSONL 为实时事件源 | **确认** — 事件名跨 session 稳定,normalize 层隔离变化 | | rate limit 来自 JSONL | **确认** — token_count 事件直接提供,无需额外 API | | stdout 解析 | **放弃** — 第一版不做 | ## 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 / 合计 | 命名为 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 为首选目标