opencode-cache-hit 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -16,6 +16,7 @@ OpenCode TUI sidebar plugin: **cache hit rate**, **tokens**, **cost**, with **su
16
16
  | Architecture | [docs/en/design.md](docs/en/design.md) | [docs/zh-CN/design.md](docs/zh-CN/design.md) |
17
17
  | Timeline / JSONL | [docs/en/timeline.md](docs/en/timeline.md) | [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md) |
18
18
  | TUI panel | [src/tui-panel/README.md](src/tui-panel/README.md) | [src/tui-panel/README.zh-CN.md](src/tui-panel/README.zh-CN.md) |
19
+ | Migration plan | [docs/en/frontend-migration-plan.md](docs/en/frontend-migration-plan.md) | [docs/zh-CN/frontend-migration-plan.md](docs/zh-CN/frontend-migration-plan.md) |
19
20
  | Contributing / npm | [CONTRIBUTING.md](CONTRIBUTING.md) | — |
20
21
  | Index | [docs/README.md](docs/README.md) | |
21
22
 
@@ -31,7 +32,8 @@ After moving or renaming exports: run full `bun test`; `tests/module-load.test.t
31
32
  ## Code conventions
32
33
 
33
34
  - **Minimal diffs**; match existing naming and module boundaries.
34
- - **Pure logic** in `stats.ts`, `timeline/`, `format-*.ts`, `tui-panel/layout.ts` — avoid pulling JSX into modules used by tests (import `layout.ts` / `palette.ts` directly, not `tui-panel/index.ts` when possible).
35
+ - **Pure logic** in `stats.ts`, `timeline/`, `format-*.ts`, `format-model.ts`, `tui-panel/layout.ts` — avoid pulling JSX into modules used by tests (import `layout.ts` / `palette.ts` directly, not `tui-panel/index.ts` when possible).
36
+ - **Sub-agent row UI**: `format-model.ts` + `agents-view.tsx`; behavior in design doc § Sub-agent row display / 子 session 行展示.
35
37
  - **`PLUGIN_ROOT`** in `load-config.ts` is `fileURLToPath(new URL("..", import.meta.url))` — do **not** wrap with an extra `dirname` (breaks config path).
36
38
  - **Sub-agent ids**: only from `session.list` overwrite in `child-session-sync.ts`; do not append via `session.get`.
37
39
  - **Agents UI totals**: child sessions only; main session excluded by design (see design doc).
package/README.md CHANGED
@@ -6,6 +6,8 @@ OpenCode **TUI sidebar plugin** for prompt **cache hit rate**, **token usage**,
6
6
 
7
7
  **Languages:** English (this file) · [简体中文](README.zh-CN.md) · [Documentation](docs/README.md)
8
8
 
9
+ ![Cache Hit sidebar panel](docs/assets/cache-hit-panel.v3.png)
10
+
9
11
  ## Why this plugin
10
12
 
11
13
  [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) already covers **main-session** cache visualization (token distribution, savings, slash-driven settings). This project exists because that scope does not fit several real workflows:
@@ -23,16 +25,12 @@ This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel
23
25
 
24
26
  The **cache TTL** feature (elapsed time display with color-coded status) is inspired by [opencode-cache-timer](https://github.com/nero-sensei/opencode-cache-timer) by nero-sensei. The original plugin provides a standalone sidebar countdown for prompt cache expiration; this plugin integrates the concept directly into the cache-hit panel.
25
27
 
26
- ## Screenshots
27
-
28
- ![Cache Hit sidebar panel](docs/assets/cache-hit-panel.png)
29
-
30
28
  ## Features
31
29
 
32
30
  - **Cache hit rate**: session total + **per-turn** rate with trend (↑ / ↓ / `-`) on the main block
33
31
  - **Token breakdown**: cache read / write / miss / output (aligned rows with visual-cache)
34
32
  - **Cost**: session cost with multi-currency config (`USD`, `CNY`, `EUR`, `GBP`, `JPY`); per-million rates and cache savings from provider config
35
- - **Sub-agents**: **Agents** section rolls up **child sessions only** (scope labeled in UI)
33
+ - **Sub-agents**: **Agents** section rolls up **child sessions only** (scope labeled in UI); each row shows model name + session ID suffix with **vendor-tinted** label (cost in muted gray)
36
34
  - **Main + Agents**: main block always shown; **Agents** section when sub-agents exist (foldable)
37
35
  - **Collapsible sections**: Detail / Model (and Agents); theme-adaptive hit bar colors
38
36
  - **i18n**: `display.lang` — `en` / `zh` / `auto` via config (no slash commands yet)
@@ -76,7 +74,7 @@ Create or edit `~/.config/opencode/tui.json` / `tui.jsonc`:
76
74
 
77
75
  Local development: use `"./plugins/opencode-cache-hit"` instead of the npm name.
78
76
 
79
- Copy `cache-hit.config.example.json` → `cache-hit.config.json` next to the plugin root ([Configuration file](#configuration-file)). **Restart OpenCode** after changing plugin code or config.
77
+ Copy `cache-hit.config.example.json` → `~/.config/opencode/cache-hit.json` (recommended) or next to the plugin root. **Restart OpenCode** after changing plugin code or config.
80
78
 
81
79
  | Install | After update |
82
80
  |---------|----------------|
@@ -122,7 +120,7 @@ Supported display currencies in config: `USD`, `CNY`, `EUR`, `GBP`, `JPY` (see `
122
120
  | `panelBorder` | `true` | Border/padding |
123
121
  | `mainHitLabel` | (i18n) | Optional override for the Hit row label |
124
122
 
125
- **Agents** totals sum **child sessions only**, not the main session (see `agentsScopeHint`). Main session metrics stay in the block above; collapse **Agents** to save space.
123
+ **Agents** totals sum **child sessions only**, not the main session (see `agentsScopeHint`). Main session metrics stay in the block above; collapse **Agents** to save space. Per-child rows use the same model slug as the main **Model** line (truncated when the sidebar is narrow); see [docs/en/design.md](docs/en/design.md) § Sub-agent row display.
126
124
 
127
125
  ### Timeline logs (`timeline`, default off)
128
126
 
@@ -238,17 +236,6 @@ src/
238
236
  tests/
239
237
  ```
240
238
 
241
- ## Configuration file
242
-
243
- Copy `cache-hit.config.example.json` → `cache-hit.config.json` next to the plugin root (`PLUGIN_ROOT`, same directory as `index.tsx`). Restart OpenCode after edits.
244
-
245
- ```bash
246
- cd ~/.cache/opencode/packages/opencode-cache-hit@latest # or your local plugin path
247
- cp cache-hit.config.example.json cache-hit.config.json
248
- ```
249
-
250
- Details (tarball contents, local paths): [CONTRIBUTING.md](CONTRIBUTING.md).
251
-
252
239
  ## Development
253
240
 
254
241
  ```bash
package/README.zh-CN.md CHANGED
@@ -2,109 +2,90 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
4
 
5
- OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量与成本;**主 session + 子 agent** 同屏汇总(默认开启主块)。可选与 [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) 共存。
5
+ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量与成本;**主 session + 子 agent** 同屏汇总(默认独立使用)。可选与 [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) 共存。
6
6
 
7
7
  **语言:** [English](README.md) · 简体中文(本页)· [文档索引](docs/README.md)
8
8
 
9
- ## 项目意图(为什么要做这个插件)
9
+ ![Cache Hit 侧边栏](docs/assets/cache-hit-panel.v3.png)
10
10
 
11
- [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) 已经很好地覆盖**主 session** 的 cache 可视化(Token 分布、节省估算、斜杠改配置等)。本仓库要补的是它**刻意不做或未覆盖**的部分:
11
+ ## 项目意图
12
12
 
13
- 1. **子 agent 可观测性** Task / explore 等会起子 session,需要把各子会话的 cache、token、费用**汇总**到侧栏。
14
- 2. **一场对话一块面板** — 主 session 与子 agent 同屏;**Agents** 段可折叠收起。
15
- 3. **离开 TUI 的分析** — 可选 **timeline JSONL**(按 assistant 轮次),便于画命中率曲线、jq 对账,而不去扒平台日志。
16
- 4. **可复用 TUI 骨架** — `src/tui-panel/` 抽出来,方便做其它侧栏插件。
13
+ [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) 已经很好地覆盖**主 session** cache 可视化(Token 分布、节省估算、斜杠命令配置等)。本项目的存在是因为其范围无法满足以下几种实际工作流:
17
14
 
18
- 后续(侧栏 Timeline 段、指标窗口、嵌套子 agent)见 [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md)[docs/zh-CN/design.md](docs/zh-CN/design.md)。
15
+ 1. **子 agent 可观测性** — OpenCode 会为 Task / explore agent 创建子 session;你需要**汇总**各子会话的 cachetoken 和费用,而非仅主线程。
16
+ 2. **一场对话一块面板** — 主 session 的命中率/token/费用**与**可折叠 **Agents** 段(子 agent 汇总)同屏展示。
17
+ 3. **离开 TUI 的分析** — 可选 **timeline JSONL**(每 assistant 轮次),用于绘图、jq 对账,无需扒取平台日志。
18
+ 4. **可复用 TUI 组件** — `src/tui-panel/` 被提取出来,方便其他侧边栏插件复用与 visual-cache 相同的布局语言。
19
19
 
20
- ## 致谢与借鉴说明
20
+ 后续规划(侧栏 Timeline 段、指标窗口、嵌套子 agent)见 [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md) 和 [docs/zh-CN/design.md](docs/zh-CN/design.md)。
21
21
 
22
- 本插件是**独立项目**,并非 opencode-visual-cache 官方维护。侧栏布局、面板组件(`src/tui-panel/`)及与 visual-cache **共存**的策略,**大量借鉴**自 [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache):
22
+ ## 致谢
23
23
 
24
- - visual-cache:主 session **上下文 / token 分布预估**
25
- - cache-hit:**按轮次指标、成本、子 agent 汇总**
24
+ 本插件**并非** opencode-visual-cache 的一部分。其侧栏布局、面板组件(`src/tui-panel/`)及共存策略**大量借鉴**自 [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache)。visual-cache 关注**主 session 上下文 / token 分布**;cache-hit 关注**按轮次指标和子 agent 汇总**。
26
25
 
27
- **缓存存活时间**功能(显示已存活时间 + 颜色状态)借鉴自 [opencode-cache-timer](https://github.com/nero-sensei/opencode-cache-timer)(作者:nero-sensei)。原插件提供独立的侧边栏倒计时;本插件将该概念直接集成到缓存命中面板中。
28
-
29
- ## 截图
30
-
31
- ![Cache Hit 侧边栏](docs/assets/cache-hit-panel.png)
32
-
33
- ## 文档
34
-
35
- | 读者 | English | 中文 |
36
- |------|---------|------|
37
- | 使用者 | [README.md](README.md) | 本文 |
38
- | 维护者 | [docs/en/design.md](docs/en/design.md) | [docs/zh-CN/design.md](docs/zh-CN/design.md) |
39
- | 时间轴 / JSONL | [docs/en/timeline.md](docs/en/timeline.md) | [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md) |
40
- | TUI 面板框架 | [src/tui-panel/README.md](src/tui-panel/README.md) | [src/tui-panel/README.zh-CN.md](src/tui-panel/README.zh-CN.md) |
41
- | 贡献 / npm | [CONTRIBUTING.md](CONTRIBUTING.md) | — |
42
- | AI 维护 | [AGENTS.md](AGENTS.md) | — |
43
- | 索引 | [docs/README.md](docs/README.md) | |
26
+ **缓存存活时间**功能(显示已缓存时长 + 颜色状态)借鉴自 [opencode-cache-timer](https://github.com/nero-sensei/opencode-cache-timer)(作者:nero-sensei)。原插件提供独立侧边栏倒计时;本插件将该概念直接集成到 cache-hit 面板中。
44
27
 
45
28
  ## 功能一览
46
29
 
47
- - **命中率**:会话累计 + 主块**单轮**命中率与趋势
48
- - **Token 明细**:缓存读/写/未命中/输出
30
+ - **命中率**:会话累计 + **单轮**命中率与趋势(↑ / ↓ / `-`)
31
+ - **Token 明细**:缓存读 / 写 / 未命中 / 输出(对齐 visual-cache 的行布局)
49
32
  - **费用**:多币种配置(`USD` / `CNY` / `EUR` / `GBP` / `JPY`);从 provider 配置读取百万 token 单价及缓存节省
50
- - **子 agent**:**Agents** 段仅汇总**子 session**(UI 有范围提示)
51
- - **主 session + Agents**:主块始终显示;有子 agent 时出现可折叠的 **Agents** 段
52
- - **可选时间轴**:按天 JSONL 落盘
53
-
54
- ## visual-cache 对比
55
-
56
- **默认独立使用**(主 session + 子 agent 同面板)。版式借鉴 visual-cache,**不依赖**该插件。
57
-
58
- | | visual-cache | cache-hit |
59
- |---|----------------|-----------|
60
- | 主 session 上下文 / Token **分布**估算 | 有 | 无 |
61
- | 按角色 Token 分布 | 有 | 无 |
62
- | 缓存**节省**、百万 token **单价** | 有 | 有(读 provider 配置) |
63
- | **斜杠命令**改配置 | 有 | 仅配置文件 |
64
- | **子 agent** 汇总 | 无 | **有** |
65
- | 按次 **JSONL** | 无 | 可选 |
66
-
67
- 完整英文对照表见 [README.md](README.md#comparison-with-opencode-visual-cache)。
33
+ - **子 agent**:**Agents** 段仅汇总**子 session**(UI 有范围标注);每行显示模型名 + session ID 后缀,**label 按厂商近似品牌色**,金额为灰色
34
+ - **主 + Agents**:主块始终显示;有子 agent 时出现可折叠的 **Agents** 段
35
+ - **可折叠段落**:Detail / Model(以及 Agents);主题自适应的命中率条颜色
36
+ - **国际化**:`display.lang` — `en` / `zh` / `auto`(配置文件,暂无斜杠命令)
37
+ - **时间轴**(可选):按天 JSONL,每 assistant 轮次一条,可用于 `jq` / 脚本分析
68
38
 
69
- ## 做什么、不做什么
39
+ ## 与 [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache) 对比
70
40
 
71
- **本插件负责**
41
+ **默认独立使用**(主 session + 子 agent 同面板)。布局借鉴 visual-cache,**不依赖**该插件。
72
42
 
73
- - session 与子 agent 的 cache / token / 成本聚合
74
- - 侧边栏 **Cache Hit** 面板(布局对齐 visual-cache)
75
- - 成本展示币种换算(默认 USD 成本 CNY 显示)
43
+ | | visual-cache | opencode-cache-hit |
44
+ |---|----------------|-------------------|
45
+ | session 上下文 / token **分布**估算 | 有 | 无(使用 visual-cache) |
46
+ | 按角色 token 分布(system / tools / …) | 有 | 无 |
47
+ | 缓存**节省**估算 | 有 | 有(从 provider 定价计算) |
48
+ | 模型**百万 token** 单价 | 有 | 有(从 SDK provider 配置读取) |
49
+ | **斜杠命令**(`/cache-lang`, `/cache-currency`, …) | 有 | 仅配置文件 |
50
+ | 折叠状态持久化(`api.kv`) | 有 | 仅内存(不持久) |
51
+ | **已加载 skill** 面板 | 有 | 无 |
52
+ | **子 agent** 会话汇总 | 无 | **有** |
53
+ | **合并**命中率(主 + 子) | 无 | 有子 agent 时显示 |
54
+ | 按次 **JSONL** 导出 | 无 | 可选 `timeline` |
76
55
 
77
- **本插件不负责**
56
+ ## 快速开始
78
57
 
79
- - session 的「预估上下文 token」分布(由 visual-cache 提供)
80
- - 修改 OpenCode 计费;`msg.cost` 仍按 `opencode.json` 美元单价
58
+ ### 方式 A:OpenCode 命令面板(推荐)
81
59
 
82
- ## 安装
83
-
84
- **方式一:** `Ctrl+P` → 输入 **install plugin** → 按 `Tab` 将范围切换为 **global**(默认是 local)→ 输入 `opencode-cache-hit@latest` → 回车。
60
+ `Ctrl+P` → 输入 **install plugin** → 按 `Tab` 切换范围为 **global**(默认是 local)→ 输入 `opencode-cache-hit@latest` → 回车。
85
61
 
86
62
  全局插件安装到 `~/.cache/opencode/packages/opencode-cache-hit@latest/`。在 `~/.config/opencode/cache-hit.json` 创建配置:
87
63
 
88
- **方式二:** 编辑 `~/.config/opencode/tui.json` / `tui.jsonc`:
64
+ ### 方式 B:手动
89
65
 
90
- ```json
66
+ 创建或编辑 `~/.config/opencode/tui.json` / `tui.jsonc`:
67
+
68
+ ```jsonc
91
69
  {
92
- "plugin": ["./plugins/opencode-cache-hit"]
70
+ "$schema": "https://opencode.ai/tui.json",
71
+ "plugin": ["opencode-cache-hit@latest"]
93
72
  }
94
73
  ```
95
74
 
96
- 复制 `cache-hit.config.example.json` `cache-hit.config.json`(与 `index.tsx` 同目录)。详见下文「[配置文件](#配置文件)」。
75
+ 本地开发:将 npm 名称替换为 `"./plugins/opencode-cache-hit"`。
76
+
77
+ 复制 `cache-hit.config.example.json` → `~/.config/opencode/cache-hit.json`(推荐)或放在插件根目录旁。更改插件代码或配置后**重启 OpenCode**。
97
78
 
98
79
  | 安装方式 | 更新后 |
99
80
  |----------|--------|
100
- | 本地路径 | 重启 |
101
- | npm `@latest` | 重启;可删 `~/.cache/opencode/packages/opencode-cache-hit@latest` |
81
+ | 本地 `./plugins/...` | 完全重启 |
82
+ | npm `@latest` | 重启;如果 UI 未更新,删除 `~/.cache/opencode/packages/opencode-cache-hit@latest` |
102
83
 
103
- 加载失败:`~/.local/share/opencode/log/`(搜 `cache-hit`)。
84
+ 加载失败:`~/.local/share/opencode/log/`(搜索 `cache-hit` 或 `failed to load tui plugin`)。
104
85
 
105
86
  ## 配置
106
87
 
107
- ### 成本(USD → 人民币)
88
+ ### 成本展示(USD → CNY 示例)
108
89
 
109
90
  ```json
110
91
  {
@@ -114,32 +95,41 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
114
95
  }
115
96
  ```
116
97
 
117
- 定价与展示同为美元:`"currency": "USD", "costUnit": "USD"`。
98
+ | 字段 | 含义 |
99
+ |-------|-------|
100
+ | `costUnit` | `msg.cost` 的币种(通常为 `USD`) |
101
+ | `currency` | 侧边栏展示币种 |
102
+ | `rate` | `costUnit` → `currency` 的汇率 |
103
+
104
+ 当无需换算时使用 `"currency": "USD", "costUnit": "USD"`。
105
+
106
+ 支持的展示币种:`USD`、`CNY`、`EUR`、`GBP`、`JPY`(见 `cache-hit.config.example.json`)。暂未实现类似 visual-cache 的 `/cache-currency` 斜杠命令。
118
107
 
119
108
  ### 展示(`display`)
120
109
 
121
110
  ```json
122
111
  "display": {
123
- "lang": "zh",
112
+ "lang": "en",
124
113
  "panelBorder": true
125
114
  }
126
115
  ```
127
116
 
128
117
  | 字段 | 默认 | 含义 |
129
- |------|------|------|
118
+ |-------|---------|------|
130
119
  | `lang` | `"en"` | `en` / `zh` / `auto` |
131
120
  | `panelBorder` | `true` | 外框与内边距 |
132
- | `mainHitLabel` | (i18n) | 可选,覆盖 Hit 行前缀 |
121
+ | `mainHitLabel` | (i18n) | 可选,覆盖 Hit 行标签 |
133
122
 
134
- **Agents 段**不含主 session token/费用(标题有「仅子会话」提示);不需要看子 agent 时折叠 **Agents** 即可。
123
+ **Agents** 段仅汇总**子 session**,不含主 session(详见 `agentsScopeHint`)。主 session 指标始终在上方块中;折叠 **Agents** 可节省空间。各子 session 行与主块 **Model** 行同源模型名(侧栏窄时会截断);详见 [docs/zh-CN/design.md](docs/zh-CN/design.md)「子 session 行展示」。
135
124
 
136
125
  ### 时间轴日志(`timeline`,默认关闭)
137
126
 
138
- 详见 [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md)。
127
+ assistant 轮次 → JSONL。详见 [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md)。
139
128
 
140
129
  ```json
141
130
  "timeline": {
142
131
  "enabled": true,
132
+ "dir": "",
143
133
  "rotateMaxBytes": 16777216,
144
134
  "retainRotated": 5,
145
135
  "maxAgeDays": 30,
@@ -147,31 +137,104 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
147
137
  }
148
138
  ```
149
139
 
150
- | 字段 | 代码默认 | 含义 |
151
- |------|----------|------|
152
- | `dir` | `""` | `logs/timeline-YYYY-MM-DD.jsonl` |
153
- | `retainRotated` | `5` | 同日大小轮转备份数 |
154
- | `maxLogFiles` | `0` | 超限时删**最早日期**日志 |
155
-
156
- ```fish
157
- set log ~/.config/opencode/plugins/opencode-cache-hit/logs/timeline-(date +%Y-%m-%d).jsonl
158
- tail -f $log
159
- # 时间字段为 ISO 8601 含本地时区(如 "2024-05-30T08:00:00.000+08:00")
160
- jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' $log
140
+ | 字段 | 默认 | 含义 |
141
+ |-------|---------|-------|
142
+ | `enabled` | `false` | 总开关 |
143
+ | `dir` | `""` | `logs/timeline-YYYY-MM-DD.jsonl`(插件根目录下) |
144
+ | `rotateMaxBytes` | `0` | 同日大小轮转至 `.jsonl.1` |
145
+ | `retainRotated` | `5` | 每日保留的轮转备份数 |
146
+ | `maxLogFiles` | `0` | 文件数量上限;删除**最早**日志 |
147
+
148
+ ```bash
149
+ LOG=~/.config/opencode/plugins/opencode-cache-hit/logs/timeline-$(date +%Y-%m-%d).jsonl
150
+ tail -f "$LOG"
151
+ # 时间字段为 ISO 8601 字符串,含本地时区(如 "2024-05-30T08:00:00.000+08:00")
152
+ jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' "$LOG"
153
+ ```
154
+
155
+ 轮转与清理:[docs/zh-CN/timeline.md#轮转与清理](docs/zh-CN/timeline.md#轮转与清理)。图表工具:[scripts/README.md](scripts/README.md)。
156
+
157
+ ### 缓存 TTL(`cacheTTL`,默认开启)
158
+
159
+ 显示 prompt cache 已存活时间。超过 TTL 时颜色变化:
160
+
161
+ - 绿色:已存活 < TTL
162
+ - 黄色:TTL ≤ 已存活 < 2×TTL
163
+ - 红色:已存活 ≥ 2×TTL
164
+
165
+ ```json
166
+ "cacheTTL": {
167
+ "enabled": true,
168
+ "providers": {
169
+ "anthropic": "5m",
170
+ "openai": "5m",
171
+ "deepseek": "2h",
172
+ "google": "1h"
173
+ }
174
+ }
161
175
  ```
162
176
 
163
- 轮转与清理:[docs/zh-CN/timeline.md § 轮转与清理](docs/zh-CN/timeline.md#轮转与清理)。
177
+ | 字段 | 默认 | 含义 |
178
+ |-------|---------|-------|
179
+ | `enabled` | `true` | 总开关 |
180
+ | `providers` | `{}` | 每个 provider 的 TTL(或 `provider:model`)。可读格式:`30s`、`5m`、`1.5h` |
181
+
182
+ **内置默认值**(配置中未指定时使用):
183
+
184
+ | Provider | 默认 TTL | 来源 |
185
+ |----------|----------|------|
186
+ | anthropic | 5 分钟 | [Anthropic 文档](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) |
187
+ | openai | 5 分钟 | [OpenAI 文档](https://platform.openai.com/docs/guides/prompt-caching) |
188
+ | deepseek | 2 小时 | [DeepSeek 文档](https://api-docs.deepseek.com/guides/kv_cache) |
189
+ | google | 1 小时 | [Google 文档](https://ai.google.dev/api/caching) |
190
+ | xai | 5 分钟 | [xAI 文档](https://docs.x.ai/developers/advanced-api-usage/prompt-caching) |
191
+ | minimax | 5 分钟 | [MiniMax 文档](https://platform.minimax.io/docs/api-reference/text-prompt-caching) |
192
+ | xiaomi | 5 分钟 | 隐式缓存 |
193
+ | qwen | 5 分钟 | 隐式缓存 |
194
+ | moonshot | 5 分钟 | 隐式缓存 |
195
+
196
+ **默认 TTL**:上表未列出的 provider 均为 5 分钟。颜色基于已存活时间与 TTL 的比值:绿色(< TTL)、黄色(TTL–2×TTL)、红色(≥ 2×TTL)。
164
197
 
165
- ## 配置文件
198
+ ## 更新
166
199
 
167
- `cache-hit.config.example.json` 复制为 `cache-hit.config.json`,放在包根目录(与 `index.tsx` 同级)。修改后需重启 OpenCode。
200
+ OpenCode 可能在首次安装时[缓存插件](https://github.com/anomalyco/opencode/issues/6774)且不自动刷新 npm 版本。
168
201
 
169
202
  ```bash
170
- cd ~/.cache/opencode/packages/opencode-cache-hit@latest # 或本地插件路径
171
- cp cache-hit.config.example.json cache-hit.config.json
203
+ rm -rf ~/.cache/opencode/packages/opencode-cache-hit@latest
172
204
  ```
173
205
 
174
- npm 打包、本地路径等说明见 [CONTRIBUTING.md](CONTRIBUTING.md)(英文)。
206
+ 然后通过 `Ctrl+P` → install plugin 重装并**重启 OpenCode**。
207
+
208
+ ## 兼容性
209
+
210
+ 模型无关:任何在消息中暴露 assistant `tokens` / `cost` 的 OpenCode provider 均可(DeepSeek、Claude、GPT 等)。数据来自 OpenCode session API,与 visual-cache 一致。
211
+
212
+ **需要**支持 TUI 插件槽位的 OpenCode(`@opencode-ai/plugin` ≥ 1.14)。可与 visual-cache 共存;运行时除 `package.json` 中声明的 peer 依赖外无额外依赖。
213
+
214
+ ## 文档索引
215
+
216
+ | 读者 | English | 中文 |
217
+ |----------|---------|------|
218
+ | 使用者 | [README.md](README.md) | 本文 |
219
+ | 维护者 | [docs/en/design.md](docs/en/design.md) | [docs/zh-CN/design.md](docs/zh-CN/design.md) |
220
+ | 时间轴 / JSONL | [docs/en/timeline.md](docs/en/timeline.md) | [docs/zh-CN/timeline.md](docs/zh-CN/timeline.md) |
221
+ | TUI 面板复用 | [src/tui-panel/README.md](src/tui-panel/README.md) | [src/tui-panel/README.zh-CN.md](src/tui-panel/README.zh-CN.md) |
222
+ | 贡献 / npm | [CONTRIBUTING.md](CONTRIBUTING.md) | — |
223
+ | AI 维护指南 | [AGENTS.md](AGENTS.md) | — |
224
+ | 总索引 | [docs/README.md](docs/README.md) | |
225
+
226
+ ## 项目结构
227
+
228
+ ```
229
+ index.tsx
230
+ cache-hit.config.example.json
231
+ src/
232
+ plugin.tsx # sidebar_content 插槽
233
+ sidebar-host.tsx # 消息、子会话同步、timeline
234
+ widget.tsx
235
+ stats.ts / timeline/ / tui-panel/
236
+ tests/
237
+ ```
175
238
 
176
239
  ## 开发
177
240
 
@@ -179,11 +242,7 @@ npm 打包、本地路径等说明见 [CONTRIBUTING.md](CONTRIBUTING.md)(英
179
242
  bun test
180
243
  ```
181
244
 
182
- 架构:[docs/zh-CN/design.md](docs/zh-CN/design.md)。贡献 / 发布:[CONTRIBUTING.md](CONTRIBUTING.md)AI 维护:[AGENTS.md](AGENTS.md)。
183
-
184
- ## 更新
185
-
186
- 若 npm 版本不刷新,参见 [OpenCode #6774](https://github.com/anomalyco/opencode/issues/6774),删除 `~/.cache/opencode/packages/opencode-cache-hit@latest` 后重装并重启。
245
+ 环境搭建、PR 规范、npm 发布见 [CONTRIBUTING.md](CONTRIBUTING.md)。架构:[docs/zh-CN/design.md](docs/zh-CN/design.md)。
187
246
 
188
247
  ## License
189
248
 
package/docs/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Documentation
2
2
 
3
- | | User guide | Design | Timeline | TUI panel |
4
- |--|------------|--------|----------|-----------|
5
- | English | [README.md](../README.md) | [en/design.md](./en/design.md) | [en/timeline.md](./en/timeline.md) | [../src/tui-panel/README.md](../src/tui-panel/README.md) |
6
- | 中文 | [README.zh-CN.md](../README.zh-CN.md) | [zh-CN/design.md](./zh-CN/design.md) | [zh-CN/timeline.md](./zh-CN/timeline.md) | [../src/tui-panel/README.zh-CN.md](../src/tui-panel/README.zh-CN.md) |
3
+ | | User guide | Design | Timeline | TUI panel | Migration |
4
+ |--|------------|--------|----------|-----------|-----------|
5
+ | English | [README.md](../README.md) | [en/design.md](./en/design.md) | [en/timeline.md](./en/timeline.md) | [../src/tui-panel/README.md](../src/tui-panel/README.md) | [en/frontend-migration-plan.md](./en/frontend-migration-plan.md) |
6
+ | 中文 | [README.zh-CN.md](../README.zh-CN.md) | [zh-CN/design.md](./zh-CN/design.md) | [zh-CN/timeline.md](./zh-CN/timeline.md) | [../src/tui-panel/README.zh-CN.md](../src/tui-panel/README.zh-CN.md) | [zh-CN/frontend-migration-plan.md](./zh-CN/frontend-migration-plan.md) |
7
7
 
8
8
  Contributing: [CONTRIBUTING.md](../CONTRIBUTING.md).
package/docs/en/design.md CHANGED
@@ -57,9 +57,10 @@ sequenceDiagram
57
57
 
58
58
  Slot->>Host: sessionId, display, api
59
59
  Host->>API: session.list → childIds
60
- API-->>Host: message.updated
61
- Host->>Host: refreshTick++
62
- Host->>API: session.messages(sid / cid)
60
+ API-->>Host: message.updated { info: Message }
61
+ Host->>Host: refreshTick++, timeline.handleMessage(info)
62
+ Host->>API: session.get(sid / cid) → aggregates
63
+ Host->>API: session.messages(sid / cid) → fallback / trend
63
64
  Host->>W: main, messages, subAgents
64
65
  W->>W: aggregate / format / TuiPanel
65
66
  ```
@@ -77,6 +78,7 @@ sequenceDiagram
77
78
  | `stats.ts` | Pure aggregation (no UI) |
78
79
  | `session-list.ts` | Parse `session.list`, `childSessionIdsForParent` |
79
80
  | `format-cost.ts` / `format-tokens.ts` / `format-cache-ui.ts` | Display formatting (`computeHitBarWidth` lives in `tui-panel/layout.ts`) |
81
+ | `format-model.ts` | Sub-agent row label (`formatSubAgentLabel`) and vendor brand colors (`modelRowColor`) |
80
82
  | `message-timing.ts` | SDK time fields |
81
83
  | `timeline/` | Per-call JSONL (`records` / `writer` / `collector`) |
82
84
  | `plugin-config.ts` / `load-config.ts` | Config normalization |
@@ -88,9 +90,9 @@ Reusable **page** skeleton aligned with visual-cache (layout, colors, foldable s
88
90
  | Module | Role |
89
91
  |--------|------|
90
92
  | `layout.ts` | Column widths, `justifyRow`, `computeHitBarWidth`, separators |
91
- | `palette.ts` | Theme → panel palette |
93
+ | `palette.ts` | Theme → panel palette; `toneBrandHex` for vendor colors on dark panels |
92
94
  | `use-panel-layout.ts` | `createPanelLayout`, `createSectionFold` |
93
- | `components.tsx` | `TuiPanel`, `TuiHitRow`, `TuiMetricRow`, … (`@opentui/solid`) |
95
+ | `components.tsx` | `TuiPanel`, `TuiHitRow`, `TuiMetricRow` (`labelFg` / `valueFg` split colors), … (`@opentui/solid`) |
94
96
  | `index.ts` | Barrel export |
95
97
 
96
98
  Import `layout.ts` / `palette.ts` directly from non-JSX code (e.g. `use-cache-hit-metrics`) so `bun test` smoke tests do not pull JSX.
@@ -112,14 +114,14 @@ flowchart TD
112
114
  F -->|no| R
113
115
  C --> H[childSessionIdsForParent]
114
116
  C2 --> H
115
- H --> I[re-read messages + aggregate]
117
+ H --> I["session.get aggregate + messages fallback"]
116
118
  ```
117
119
 
118
120
  - **Single source of truth**: `childIds` always **replaced** from `session.list` (no `session.get` append).
119
121
  - **Races**: `listGen` increments on parent change; callbacks check generation and `parentId`.
120
122
  - **Streaming**: foreign-session `message.updated` is debounced (`CHILD_LIST_DEBOUNCE_MS` = 200ms).
121
123
  - **Depth**: direct children only (`parentID === sid`); nested sub-agents are future work.
122
- - **Data**: `messages(cid)` → `aggregateSessionFromMessages`; entries without stats are filtered out.
124
+ - **Data**: `session.get(cid)` → `aggregateFromSessionObject` (primary); falls back to `messages(cid)` → `aggregateSessionFromMessages`. Model/provider metadata is patched from messages when missing from session aggregates (`withModelFallback`).
123
125
 
124
126
  ### Agents block semantics
125
127
 
@@ -130,18 +132,35 @@ flowchart TD
130
132
 
131
133
  The UI shows `agentsScopeHint` (“sub-sessions only”). This complements visual-cache’s main-session view—it is not a missing-aggregation bug.
132
134
 
135
+ ### Sub-agent row display
136
+
137
+ When multiple child sessions run in parallel, each active sub-agent gets a metric row under **Agents** (see screenshot `docs/assets/cache-hit-panel.v3.png`).
138
+
139
+ | Aspect | Behavior |
140
+ |--------|----------|
141
+ | **Purpose** | Distinguish which child session and which model in a narrow sidebar |
142
+ | **Label text** | `{displayModelName} …{sessionIdTail}` — same naming rules as the main **Model** row (`shortModelName` + date-suffix strip); **no** semantic nicknames (`ds-flash`, etc.) |
143
+ | **Truncation** | `gauge` ≈ measured panel width − border gutter; label budget subtracts right-side cost/tokens. Prefer keeping the **model prefix**; shrink ID tail (6 → 4 chars) before dropping the tail |
144
+ | **Label color** | Approximate **vendor brand** hex (`MODEL_BRAND_HEX` in `format-model.ts`), passed through `toneBrandHex` for legibility on dark terminals |
145
+ | **Value color** | `muted` — separate from label so row cost is not confused with Agents total **Cost** (`success` green) |
146
+ | **Family match** | `MODEL_FAMILY_RULES` (claude, deepseek, openai, gemini, qwen, glm, kimi, minimax, grok, mimo, meta, mistral); matching is **case-insensitive** on model slug and `providerID` |
147
+ | **Unknown models** | Stable hash → neutral fallback hues (never `success`) |
148
+ | **Config** | No `cache-hit.json` keys in v1; extend `MODEL_FAMILY_RULES` / `MODEL_BRAND_HEX` in code |
149
+
150
+ Implementation: `agents-view.tsx` calls `formatSubAgentLabel` + `modelRowColor`; `TuiMetricRow` uses `labelFg` / `valueFg`.
151
+
133
152
  ## Aggregation and refresh
134
153
 
135
154
  ### When values recompute
136
155
 
137
156
  | Data | Trigger |
138
157
  |------|---------|
139
- | Main snapshot | `createMemo` reads `refreshTick` + `messages(sid)` |
140
- | Main messages (Hit trend) | Same |
141
- | Sub-agent content | `childIds` or `refreshTick` → re-read each `messages(cid)` |
158
+ | Main snapshot | `createMemo` reads `refreshTick` + `session.get(sid)` (aggregate); fallback `messages(sid)` |
159
+ | Main messages (Hit trend) | `createMemo` reads `refreshTick` + `messages(sid)` |
160
+ | Sub-agent content | `refreshTick` + `childIds` → `session.get(cid)` per child; fallback `messages(cid)` |
142
161
  | Sub-agent ids | `session.list` callback / debounced refresh on foreign activity |
143
162
 
144
- `sidebar-host` subscribes to `message.updated` and bumps `refreshTick` so streaming token updates always recompute.
163
+ `sidebar-host` subscribes to `message.updated` and bumps `refreshTick` so dependent memos recompute; session totals follow `session.get()` aggregates, while per-turn trend follows recent messages.
145
164
 
146
165
  ### Accumulation rules
147
166
 
@@ -0,0 +1,100 @@
1
+ # Frontend Migration: session.get() Aggregates
2
+
3
+ ## Overview
4
+
5
+ Cache-hit previously derived all cost/token statistics by iterating `api.state.session.messages()` and summing per-message fields. This path is capped at **100 messages** by the OpenCode TUI sync layer ([issue #31513]), silently truncating sessions with >100 messages.
6
+
7
+ **Impact**: For a session exceeding 100 messages, the cap causes significant data loss. In a real long-running session, the plugin showed only ~43% of actual cache-read tokens and ~39% of actual cost.
8
+
9
+ | Metric | Old (messages, 100-cap) | New (session.get, no cap) |
10
+ |--------|------------------------|--------------------------|
11
+ | Cache read | 59.6M tok | **139.0M tok** |
12
+ | Cache write | 89.8K tok | **1.4M tok** |
13
+ | Miss (input) | 118 tok | **329 tok** |
14
+ | Output | 32.7K tok | **91.9K tok** |
15
+ | Sub-agents visible | 3 | **10** |
16
+
17
+ The fix swaps the data source from per-message iteration to `api.state.session.get()`, which returns **database-level aggregates** computed from all messages by the session projector — not affected by the 100-message cap.
18
+
19
+ ## Root Cause
20
+
21
+ OpenCode's TUI sync (`packages/opencode/src/cli/cmd/tui/context/sync.tsx`) hardcodes `limit: 100` when fetching session messages:
22
+
23
+ ```typescript
24
+ // Line 559 — fetches at most 100 messages
25
+ sdk.client.session.messages({ sessionID, limit: 100 }),
26
+
27
+ // Lines 580-581 — TUI store also keeps only last 100
28
+ const visible = infos.slice(-100)
29
+ ```
30
+
31
+ `api.state.session.messages(sid)` reads from `sync.data.message[sid]`, which holds at most 100 entries. For sessions exceeding this, plugins see incomplete data with no indication of truncation.
32
+
33
+ Meanwhile, the `session` table in OpenCode's SQLite database stores **pre-aggregated** columns (`cost`, `tokens_input`, `tokens_output`, `tokens_cache_read`, `tokens_cache_write`) updated via SQL increments by the session projector — every message, no cap.
34
+
35
+ `api.state.session.get(sid)` returns the full `Session` object (SDK type `Session`), which includes `cost` and `tokens` fields populated from these database aggregates.
36
+
37
+ ## Solution
38
+
39
+ ### Design
40
+
41
+ ```
42
+ Before:
43
+ api.state.session.messages(sid) → iterate 100 msgs → sum ❌ capped
44
+
45
+ After:
46
+ api.state.session.get(sid) → read cost/tokens directly → done ✅ full
47
+ └─ session not available? → fallback to messages (compat)
48
+ ```
49
+
50
+ ### Changes
51
+
52
+ | File | Change |
53
+ |------|--------|
54
+ | `src/types.ts` | New `SessionObject` type (`model`, `cost`, `tokens`, `parentID`); updated `session.get` return type |
55
+ | `src/stats.ts` | New `aggregateFromSessionObject()` — O(1) extraction from `SessionObject` → `SessionSnapshot` |
56
+ | `src/sidebar-host.tsx` | `mainSnap` and `subAgentList` preferred `session.get()` with fallback to message iteration |
57
+ | `tests/stats.test.ts` | 3 test cases covering empty, full, and partial session objects |
58
+
59
+ ### Data Flow
60
+
61
+ 1. **`mainSnap`**: Calls `api.state.session.get(sid)` → `aggregateFromSessionObject()` → returns snapshot if stats present. If session unavailable or empty, falls back to `aggregateSessionFromMessages()`.
62
+
63
+ 2. **`subAgentList`**: Same pattern per child session ID.
64
+
65
+ 3. **Per-call trend** (`computePerCallHitTrend`): Still uses `api.state.session.messages()` — trend only needs the last few turns, so 100 messages is sufficient.
66
+
67
+ ### Why session.get() works for resumed sessions
68
+
69
+ When resuming a session, OpenCode's TUI `sync()` immediately calls `sdk.client.session.get({ sessionID })` which returns the full `Session` object with database-level aggregates. This object is stored in the TUI state store, and `api.state.session.get(sid)` reads it. The plugin sees accurate totals from the moment the session loads.
70
+
71
+ ### Fallback rationale
72
+
73
+ `session.get()` returns `undefined` for sessions that have never been synced to the TUI state (rare, but possible for child sessions loaded in a different context). The fallback to `api.state.session.messages()` preserves existing behavior as a safety net.
74
+
75
+ ## Performance
76
+
77
+ | Metric | Old (message iteration) | New (session.get) |
78
+ |--------|------------------------|-------------------|
79
+ | Time complexity | O(n) where n ≤ 100 | O(1) |
80
+ | Memory | Reads entire message array | Reads one small object |
81
+ | Reactivity trigger | `message.updated` → recompute | Same event, same timing |
82
+ | Data completeness | ≤ 100 messages | All messages (DB aggregate) |
83
+
84
+ ## Rollout
85
+
86
+ 1. **No config changes required** — the plugin automatically uses `session.get()` when available.
87
+ 2. **No migration step** for existing users — old config files and timeline logs are unaffected.
88
+ 3. **Restart OpenCode** to pick up the updated plugin code.
89
+ 4. **Verify**: Open a session with >100 messages; the sidebar should show totals matching database aggregates.
90
+
91
+ ## Related
92
+
93
+ - [anomalyco/opencode#31513] — v1 TUI sync hardcodes limit:100
94
+ - [anomalyco/opencode#26861] — PR adding cursor-based pagination (pending)
95
+ - [anomalyco/opencode#6548] — Paginated message loading feature request
96
+
97
+ [issue #31513]: https://github.com/anomalyco/opencode/issues/31513
98
+ [anomalyco/opencode#31513]: https://github.com/anomalyco/opencode/issues/31513
99
+ [anomalyco/opencode#26861]: https://github.com/anomalyco/opencode/pull/26861
100
+ [anomalyco/opencode#6548]: https://github.com/anomalyco/opencode/issues/6548