opencode-cache-hit 0.2.0 → 0.2.1

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.
@@ -131,7 +131,7 @@ Example values above; code defaults below (`enabled: false`, rotation `0` except
131
131
  1. Optional size roll **before** append.
132
132
  2. `appendFile` one JSON line.
133
133
  3. Optional line trim **after** append.
134
- 4. Async flush in `queueMicrotask`; `flushedKeys` dedupe by `messageKey` (not cleared on session switch; cleared on date change).
134
+ 4. Event-driven: `message.updated` `handleMessage()` fire-and-forget `appendFile`. No polling, no dedup.
135
135
 
136
136
  ## Rotation and retention
137
137
 
@@ -169,11 +169,11 @@ Does **not** match legacy `ses_*.jsonl` names.
169
169
 
170
170
  New filename after midnight; previous days remain until cleanup runs.
171
171
 
172
- ### Dedup and session switch
172
+ ### Collection
173
173
 
174
- - One append per `messageKey` per process (`flushedKeys`).
175
- - Switching main session: same day file; filter by `rootSessionId`.
176
- - **Restart** clears `flushedKeys`; completed messages may be written again (no persistent dedupe yet).
174
+ - `message.updated` event carries the full `Message` object. The collector subscribes directly — no polling, no dedup.
175
+ - Switching main session: `resetForRootChange()` clears in-memory cache; events for the new session arrive naturally.
176
+ - Restarts are safe: messages before startup were already written to JSONL in the previous session. No replay, no scan.
177
177
 
178
178
  ## Runtime wiring
179
179
 
@@ -181,19 +181,18 @@ New filename after midnight; previous days remain until cleanup runs.
181
181
  sequenceDiagram
182
182
  participant E as message.updated
183
183
  participant H as sidebar-host
184
- participant B as timeline/build
184
+ participant C as timeline/collector
185
185
  participant W as timeline/writer
186
186
 
187
- E->>H: refreshTick++
188
- H->>B: debounce 500ms buildCallRecords
189
- alt enabled and complete and not flushed
190
- B->>W: append JSONL
187
+ E->>H: { sessionID, info: Message }
188
+ H->>C: handleMessage(sessionID, info)
189
+ alt assistant and complete
190
+ C->>W: append JSONL (fire-and-forget)
191
191
  end
192
192
  ```
193
193
 
194
- - Child ids from `child-session-sync` / `session.list`; timeline only reads `messages()`.
195
- - Debounce 500ms when enabled.
196
- - Scope: current TUI root session + its children.
194
+ - Child ids from `child-session-sync` / `session.list`; timeline writes main and child sessions from events.
195
+ - No debounce, no polling. Scope: current TUI root session + its children.
197
196
 
198
197
  ## UI phases
199
198
 
@@ -214,8 +213,13 @@ python3 -c "import json,sys; r=[json.loads(x) for x in open(sys.argv[1]) if x.st
214
213
 
215
214
  bun scripts/plot-hit-rate.ts "$LOG" -o /tmp/hit.svg
216
215
  bun scripts/plot-hit-rate.ts "$LOG" --by-root -o /tmp/hit-multi.svg
216
+
217
+ # interactive HTML dashboard (filters, Chart.js); add --open to launch browser
218
+ bun scripts/timeline-dashboard.ts --open
217
219
  ```
218
220
 
221
+ Default log dir matches `timeline.dir` in plugin config (`~/.local/share/opencode/logs/cache-hit/`).
222
+
219
223
  ### Phase 2 — sidebar Timeline section (planned)
220
224
 
221
225
  ### Phase 3 — metric window linkage (planned)
@@ -57,8 +57,8 @@ 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++
60
+ API-->>Host: message.updated { info: Message }
61
+ Host->>Host: refreshTick++, timeline.handleMessage(info)
62
62
  Host->>API: session.messages(sid / cid)
63
63
  Host->>W: main, messages, subAgents
64
64
  W->>W: aggregate / format / TuiPanel
@@ -77,6 +77,7 @@ sequenceDiagram
77
77
  | `stats.ts` | 纯函数聚合(无 UI) |
78
78
  | `session-list.ts` | `session.list` 响应解析、`childSessionIdsForParent` |
79
79
  | `format-cost.ts` / `format-tokens.ts` / `format-cache-ui.ts` | 展示格式化(**不含** `computeHitBarWidth`,其在 `tui-panel/layout.ts`) |
80
+ | `format-model.ts` | 子 agent 行 label(`formatSubAgentLabel`)与厂商品牌色(`modelRowColor`) |
80
81
  | `message-timing.ts` | SDK 时间字段辅助 |
81
82
  | `timeline/` | 按次 JSONL(`records` / `writer` / `collector`) |
82
83
  | `plugin-config.ts` / `load-config.ts` | 配置归一化与默认值 |
@@ -88,9 +89,9 @@ sequenceDiagram
88
89
  | 模块 | 职责 |
89
90
  |------|------|
90
91
  | `layout.ts` | 视觉列宽、`justifyRow`、`computeHitBarWidth`、分隔线 |
91
- | `palette.ts` | 主题色 → 面板调色板 |
92
+ | `palette.ts` | 主题色 → 面板调色板;`toneBrandHex` 用于深色面板上的厂商色 |
92
93
  | `use-panel-layout.ts` | `createPanelLayout`(测宽)、`createSectionFold` |
93
- | `components.tsx` | `TuiPanel`、`TuiHitRow`、`TuiMetricRow` 等(需 `@opentui/solid`) |
94
+ | `components.tsx` | `TuiPanel`、`TuiHitRow`、`TuiMetricRow`(`labelFg` / `valueFg` 分段上色)等(需 `@opentui/solid`) |
94
95
  | `index.ts` | 对外 barrel |
95
96
 
96
97
  纯逻辑模块(如 `use-cache-hit-metrics`)应从 `layout.ts` / `palette.ts` 直接 import,避免经 `index.ts` 拉入 JSX(便于 `bun test` 冒烟)。
@@ -130,6 +131,23 @@ flowchart TD
130
131
 
131
132
  主 session 若仍有编排类调用,其 token/费用不会出现在 Agents 合计中;UI 通过 `agentsScopeHint`(「仅子会话 / sub-sessions」)标明。与 visual-cache 对主 session 的展示互补,不是漏算 bug。
132
133
 
134
+ ### 子 session 行展示
135
+
136
+ 多个子 session 并行时,**Agents** 段下每个有活动的子 session 一行(见截图 `docs/assets/cache-hit-panel.v3.png`)。
137
+
138
+ | 方面 | 行为 |
139
+ |------|------|
140
+ | **目的** | 在窄侧栏中辨认「哪条子 session、用的什么模型」 |
141
+ | **Label 文案** | `{displayModelName} …{sessionIdTail}` — 与主块 **Model** 行同源(`shortModelName` + 去日期尾);**不做**语义缩写(如 `ds-flash`) |
142
+ | **截断** | `gauge` ≈ 实测面板宽 − 边框 gutter;label 预算再减去右侧 cost/tok。优先保留**模型前缀**;先缩短 ID 尾(6 → 4 字符) |
143
+ | **Label 颜色** | 厂商近似品牌色(`MODEL_BRAND_HEX`,经 `toneBrandHex` 压低饱和度以适配深色终端) |
144
+ | **金额颜色** | `muted`,与 label 分开,避免与 Agents 合计 **Cost** 的 `success` 绿色混淆 |
145
+ | **Family 匹配** | `MODEL_FAMILY_RULES`(claude、deepseek、openai、gemini、qwen、glm、kimi、minimax、grok、mimo、meta、mistral);模型 slug 与 `providerID` **大小写不敏感** |
146
+ | **未知模型** | 稳定 hash → 中性 fallback 色(不用 `success`) |
147
+ | **配置** | v1 无 `cache-hit.json` 项;在代码中扩展 `MODEL_FAMILY_RULES` / `MODEL_BRAND_HEX` |
148
+
149
+ 实现:`agents-view.tsx` 调用 `formatSubAgentLabel`、`modelRowColor`;`TuiMetricRow` 使用 `labelFg` / `valueFg`。
150
+
133
151
  ## 聚合与刷新
134
152
 
135
153
  ### 何时重算
@@ -223,7 +241,7 @@ flowchart TD
223
241
  | 指标切换 | 累计 / 最近 N 轮 / 滑动窗口;与时间轴 Phase 3 联动 | timeline.md § Phase 3 |
224
242
  | 子 agent | 递归子 session、按 agent 类型过滤 | timeline.md § 风险;侧栏另议 |
225
243
 
226
- 实现日志时继续复用 `message.updated` + `messages()`;落盘异步、勿阻塞 TUI(见 timeline.md)。
244
+ 实现日志时使用 `message.updated` 事件直接驱动的 `handleMessage()`;落盘异步、勿阻塞 TUI(见 timeline.md)。
227
245
 
228
246
  ## 插件缓存
229
247
 
@@ -153,7 +153,7 @@ export function buildCallRecords(
153
153
  1. 可选 `rotateMaxBytes`:写**前**若当日活跃文件 ≥ 阈值 → 链式 rename(见 § 轮转与清理)。
154
154
  2. `appendFile` 一行 JSON。
155
155
  3. 可选 `maxLinesPerFile`:写**后**读回活跃文件,只保留最后 N 行(**删行**,不生成 `.1`)。
156
- 4. 异步:`collector` `queueMicrotask` 里写盘;`flushedKeys` `messageKey` 去重(切换主 session **不**清空;跨日换文件名时清空)。
156
+ 4. 事件驱动:`message.updated` `handleMessage()` fire-and-forget `appendFile`。无轮询,无去重。
157
157
 
158
158
  ## 轮转与清理
159
159
 
@@ -193,11 +193,11 @@ export function buildCallRecords(
193
193
 
194
194
  午夜后自动写入新文件名;昨日文件保留,直至上述清理策略删除。
195
195
 
196
- ### 去重与切换 session
196
+ ### 收集
197
197
 
198
- - 同一 `messageKey` append 一次(进程内 `flushedKeys`)。
199
- - 切换 TUI session:仍写**同日**文件,用 `rootSessionId` 过滤;**不**因切换而重复写同一 `messageKey`。
200
- - 插件重启后 `flushedKeys` 为空,可能对**同一批已完成消息**再写一遍(若需避免,需另做持久化去重,当前未做)。
198
+ - `message.updated` 事件携带完整 `Message` 对象。collector 直接订阅事件——无轮询,无去重。
199
+ - 切换主 session:`resetForRootChange()` 清空内存缓存;新 session 的事件自然到达。
200
+ - 重启安全:启动前的消息已在上次 session 中写入 JSONL。无需回放,无需扫描。
201
201
 
202
202
  ## 运行时接入
203
203
 
@@ -205,20 +205,18 @@ export function buildCallRecords(
205
205
  sequenceDiagram
206
206
  participant E as message.updated
207
207
  participant H as sidebar-host
208
- participant B as timeline/build
208
+ participant C as timeline/collector
209
209
  participant W as timeline/writer
210
210
 
211
- E->>H: refreshTick++(现有)
212
- H->>B: debounce 500ms buildCallRecords(main+children)
213
- B->>H: 更新 memoryRecords(Signal)
214
- alt enabled and isComplete and not flushed
215
- B->>W: append JSONL
211
+ E->>H: { sessionID, info: Message }
212
+ H->>C: handleMessage(sessionID, info)
213
+ alt assistant and complete
214
+ C->>W: append JSONL(fire-and-forget)
216
215
  end
217
216
  ```
218
217
 
219
- - **与 `child-session-sync` 分工**:子 id 列表仍由 `session.list` 负责;时间轴只读 `messages()`,不额外 list
220
- - **Debounce**:500ms(比 child list 200ms 略长,减少流式写盘);仅 `timeline.enabled` 时注册。
221
- - **作用域**:只记录「当前 TUI 绑定的 `rootSessionId`」及其子 session;落盘路径按**当天**不变,切换主 session 仍写同一日文件。
218
+ - **与 `child-session-sync` 分工**:子 id 列表仍由 `session.list` 负责;时间轴从事件中写 main 和 child session
219
+ - debounce,无轮询。**作用域**:当前 TUI root session + 其子 session。
222
220
 
223
221
  ## UI(分阶段)
224
222
 
@@ -234,15 +232,20 @@ tail -f $LOG
234
232
  jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' $LOG
235
233
  ```
236
234
 
237
- **画图(可选脚本)** — 见 [scripts/README.md](../../scripts/README.md):
235
+ **画图 / 分析(可选脚本)** — 见 [scripts/README.md](../../scripts/README.md):
238
236
 
239
237
  ```bash
240
238
  python3 -c "import json,sys; r=[json.loads(x) for x in open(sys.argv[1]) if x.strip()]; h=[x['hitPercent'] for x in r if x.get('hitPercent') is not None]; print(f\"{len(r)} calls, avg hit {sum(h)/len(h):.1f}%\")" $LOG
241
239
 
242
240
  bun scripts/plot-hit-rate.ts $LOG -o /tmp/hit.svg
243
241
  bun scripts/plot-hit-rate.ts $LOG --by-root -o /tmp/hit-multi.svg
242
+
243
+ # 交互式 HTML 仪表盘(筛选、Chart.js);加 --open 才会打开浏览器
244
+ bun scripts/timeline-dashboard.ts --open
244
245
  ```
245
246
 
247
+ 默认日志目录与插件 `timeline.dir` 一致(`~/.local/share/opencode/logs/cache-hit/`)。
248
+
246
249
  ### Phase 2 — 侧栏「Timeline」折叠段
247
250
 
248
251
  - 在 `widget.tsx` 增加 `TuiSection`,展示最近 `maxMemoryRows` 条(窄屏每行一条):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cache-hit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "OpenCode TUI sidebar: prompt cache hit rate, tokens & cost with sub-agent rollup. Works with opencode-visual-cache; optional per-call JSONL timeline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/scripts/README.md CHANGED
@@ -20,6 +20,69 @@ Export TSV for spreadsheets (time fields are ISO 8601 strings):
20
20
  jq -r 'select(.hitPercent!=null) | [.completedAt,.scope,.hitPercent,.cost]|@tsv' logs/timeline-2026-05-31.jsonl
21
21
  ```
22
22
 
23
+ ## `timeline-dashboard.ts` (Bun, no install)
24
+
25
+ Interactive HTML dashboard with charts, filters, and data tables:
26
+
27
+ ```bash
28
+ bun scripts/timeline-dashboard.ts # auto-detect logs/ (no browser)
29
+ bun scripts/timeline-dashboard.ts --open # write HTML, then open browser
30
+ bun scripts/timeline-dashboard.ts -o /tmp/report.html # custom output path
31
+ bun scripts/timeline-dashboard.ts ~/logs/timeline-*.jsonl # globs expanded by script
32
+ bun scripts/timeline-dashboard.ts --output /tmp/report.html --open # combined
33
+ ```
34
+
35
+ Default output: `/tmp/timeline-dashboard-YYYY-MM-DD-HHmmss.html` (timestamp suffix to avoid overwrites).
36
+
37
+ **Browser:** not opened by default; pass `--open` (macOS `open`, Linux `xdg-open`, Windows `start`).
38
+
39
+ **Features:**
40
+ - Summary cards follow active filters (records, tokens, cost, avg hit rate)
41
+ - Time / session / scope / model / text search filters
42
+ - 3 Chart.js charts: token volume (stacked bar), hit rate + cost (dual axis), duration (bar)
43
+ - Session summary table (mixed main+child scope shown as `main+child`)
44
+ - Per-call detail table with expandable rows (all JSONL fields)
45
+ - Embedded data — no server needed, just open the HTML file
46
+
47
+ **How it works:**
48
+
49
+ 1. Reads `timeline-*.jsonl` and rotation backups `timeline-*.jsonl.N` from the default log dir (`~/.local/share/opencode/logs/cache-hit/`) or user-supplied paths/globs
50
+ 2. Parses each JSONL line (`schema: 1` validation), sorts by `completedAt` / `created`
51
+ 3. Aggregates per-session statistics in the browser when filters change
52
+ 4. Generates a self-contained HTML file with:
53
+ - All data embedded as JSON in a `<script>` tag (`<` escaped for safety)
54
+ - Chart.js 4.4.7 from CDN (`cdn.jsdelivr.net`)
55
+ - Vanilla JS for interactivity (no framework dependency)
56
+ 5. Optional `--open` opens the output file in the default browser
57
+
58
+ **Avg hit rate:** excludes `skippedForHit` rows (same rule as `plot-hit-rate.ts`); null `hitPercent` still appear in tables/charts.
59
+
60
+ **Cost display:** reads `currency` / `costUnit` / `rate` from `~/.config/opencode/cache-hit.json` when present (same as the TUI sidebar). **No config file** → defaults (`CNY` display, `USD` JSONL unit, rate `6.77`). Invalid or partial cost fields are normalized; corrupt config falls back without failing the script. JSONL always stores raw `cost` in `costUnit` (usually USD).
61
+
62
+ **Note:** The generated HTML is self-contained except Chart.js CDN. Re-run the script to refresh data (static snapshot).
63
+
64
+ ### Design approaches
65
+
66
+ **Option A (current — static HTML):**
67
+ Data is embedded into the HTML at build time by Bun. The browser just renders.
68
+
69
+ ```
70
+ bun scripts/timeline-dashboard.ts → /tmp/timeline-dashboard-YYYY-MM-DD-HHmmss.html (self-contained)
71
+ ```
72
+
73
+ - Pros: No server needed, zero runtime deps, can email/share the file
74
+ - Cons: Snapshot only — re-run to refresh data
75
+
76
+ **Option B (live server):**
77
+ Starts an HTTP server; the browser fetches JSONL via `fetch("/api/logs")`.
78
+
79
+ ```
80
+ bun scripts/timeline-dashboard.ts serve → http://localhost:PORT
81
+ ```
82
+
83
+ - Pros: Live reload — refresh the page to see new logs
84
+ - Cons: Must keep a process running, cannot send the page as a file
85
+
23
86
  ## `plot-hit-rate.ts` (Bun, no install)
24
87
 
25
88
  Terminal ASCII chart; optional SVG (`open /tmp/hit.svg`):