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

610 lines
23 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 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` 真实 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. 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。
**验证方法:**
```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 <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.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"
# 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_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')` 兜底
**当前状态2026-03-22需在真实 TTY 中手工验证。**
在真实终端(非 Claude Code 子 shell中运行
```bash
# 保存到 /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 为首选目标