opencode-visual-cache 1.0.5 → 1.0.6

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/dist/index.js CHANGED
@@ -57,6 +57,7 @@ const T = LANG_ZH ? {
57
57
  cost: "费用:",
58
58
  saved: "累计节省:",
59
59
  model: "模型:",
60
+ provider: "提供商:",
60
61
  rate: "单价:",
61
62
  hitFolded: "命中",
62
63
  inputRate: "输入",
@@ -72,6 +73,7 @@ const T = LANG_ZH ? {
72
73
  cost: "Cost:",
73
74
  saved: "Total Saved:",
74
75
  model: "Model:",
76
+ provider: "Provider:",
75
77
  rate: "Rate:",
76
78
  hitFolded: "hit",
77
79
  inputRate: "in",
@@ -201,10 +203,19 @@ function TokenCachePanel(props) {
201
203
  let cost = 0;
202
204
  let pid = "";
203
205
  let mid = "";
206
+ // Track individual hit rates per assistant message to compute trend
207
+ let prevMsgHitRate = -1;
208
+ let lastMsgHitRate = -1;
204
209
  for (const msg of msgs) {
205
210
  if (msg.role !== "assistant")
206
211
  continue;
207
212
  const t = msg.tokens;
213
+ const msgInputTokens = num(t.input) + num(t.cache?.read);
214
+ const msgReadTokens = num(t.cache?.read);
215
+ if (msgInputTokens > 0) {
216
+ prevMsgHitRate = lastMsgHitRate;
217
+ lastMsgHitRate = (msgReadTokens / msgInputTokens) * 100;
218
+ }
208
219
  input += num(t.input);
209
220
  read += num(t.cache?.read);
210
221
  write += num(t.cache?.write);
@@ -240,10 +251,18 @@ function TokenCachePanel(props) {
240
251
  const hitRate = totalInput > 0 ? (read / totalInput) * 100 : 0;
241
252
  const model = mid.split("/").pop() ?? mid;
242
253
  const hasPricing = inputRate > 0 || cacheReadRate > 0;
254
+ let trend = 0;
255
+ const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
256
+ if (hasTrendData) {
257
+ trend = lastMsgHitRate - prevMsgHitRate;
258
+ }
259
+ const providerName = pid || "";
243
260
  return {
244
261
  hitRate, read, write, freshInput: input, output,
245
262
  cost, saved, model, inputRate, cacheReadRate, hasPricing,
246
- hasData: read > 0 || write > 0 || cost > 0,
263
+ hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
264
+ trend, hasTrendData,
265
+ providerName,
247
266
  };
248
267
  });
249
268
  const d = () => data();
@@ -272,8 +291,12 @@ function TokenCachePanel(props) {
272
291
  return pal().error;
273
292
  });
274
293
  const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - GUTTER)));
294
+ function trendLabel(t) {
295
+ return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "");
296
+ }
275
297
  const barW = createMemo(() => {
276
- const overhead = visualWidth(T.hit) + 1 + 2 + 1 + /*pct*/ 5 + GUTTER;
298
+ const trendSpace = d().hasTrendData ? 1 + visualWidth(trendLabel(d().trend)) : 0;
299
+ const overhead = visualWidth(T.hit) + 1 + 2 + 1 + /*pct*/ 5 + trendSpace + GUTTER;
277
300
  return Math.max(3, panelWidth() - overhead);
278
301
  });
279
302
  const bar = createMemo(() => progressBar(d().hitRate, barW()));
@@ -288,7 +311,7 @@ function TokenCachePanel(props) {
288
311
  return (_jsx(Show, { when: d().hasData, children: _jsxs("box", { border: true, borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: 2, paddingRight: 2, flexDirection: "column", gap: 0, onSizeChange: function () {
289
312
  const w = Math.max(MIN_PANEL_WIDTH, this.width);
290
313
  setPanelWidth((prev) => (prev === w ? prev : w));
291
- }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => !o), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.title }) }), _jsxs(Show, { when: !open(), children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] }), _jsxs(Show, { when: open(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [T.hit, " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() })] }), _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(d().read), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(d().write), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(d().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(d().output), T.tok) }), _jsx("text", { fg: pal().muted, children: sep() }), _jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(d().cost)) }), _jsx(Show, { when: d().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: T.saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(d().saved)))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(d().saved)] })] }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, d().model) }), _jsxs(Show, { when: d().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, "$" + d().inputRate.toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: d().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + d().cacheReadRate.toFixed(2) + "/M " + T.cacheRate) }) })] })] })] }) }));
314
+ }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => !o), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.title }) }), _jsxs(Show, { when: !open(), children: [_jsxs(Show, { when: d().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(d().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] }), _jsxs("span", { style: { fg: d().trend !== 0 ? (d().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(d().trend)] })] }), _jsxs(Show, { when: !d().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] })] }), _jsxs(Show, { when: open(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [T.hit, " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: d().hasTrendData, children: _jsxs("span", { style: { fg: d().trend !== 0 ? (d().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(d().trend)] }) })] }), _jsx(Show, { when: d().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(d().read), T.tok) }) }), _jsx(Show, { when: d().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(d().write), T.tok) }) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(d().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(d().output), T.tok) }), _jsx("text", { fg: pal().muted, children: sep() }), _jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(d().cost)) }), _jsx(Show, { when: d().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: T.saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(d().saved)))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(d().saved)] })] }) }), _jsx(Show, { when: d().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, d().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, d().model) }), _jsxs(Show, { when: d().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, "$" + d().inputRate.toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: d().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + d().cacheReadRate.toFixed(2) + "/M " + T.cacheRate) }) })] })] })] }) }));
292
315
  }
293
316
  // ---------------------------------------------------------------------------
294
317
  // Plugin entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.tsx CHANGED
@@ -73,6 +73,7 @@ const T = LANG_ZH ? {
73
73
  cost: "费用:",
74
74
  saved: "累计节省:",
75
75
  model: "模型:",
76
+ provider: "提供商:",
76
77
  rate: "单价:",
77
78
  hitFolded: "命中",
78
79
  inputRate: "输入",
@@ -88,6 +89,7 @@ const T = LANG_ZH ? {
88
89
  cost: "Cost:",
89
90
  saved: "Total Saved:",
90
91
  model: "Model:",
92
+ provider: "Provider:",
91
93
  rate: "Rate:",
92
94
  hitFolded: "hit",
93
95
  inputRate: "in",
@@ -229,9 +231,19 @@ function TokenCachePanel(props: {
229
231
  let pid = ""
230
232
  let mid = ""
231
233
 
234
+ // Track individual hit rates per assistant message to compute trend
235
+ let prevMsgHitRate = -1
236
+ let lastMsgHitRate = -1
237
+
232
238
  for (const msg of msgs) {
233
239
  if (msg.role !== "assistant") continue
234
240
  const t = (msg as AssistantMessage).tokens
241
+ const msgInputTokens = num(t.input) + num(t.cache?.read)
242
+ const msgReadTokens = num(t.cache?.read)
243
+ if (msgInputTokens > 0) {
244
+ prevMsgHitRate = lastMsgHitRate
245
+ lastMsgHitRate = (msgReadTokens / msgInputTokens) * 100
246
+ }
235
247
  input += num(t.input)
236
248
  read += num(t.cache?.read)
237
249
  write += num(t.cache?.write)
@@ -267,10 +279,20 @@ function TokenCachePanel(props: {
267
279
  const model = mid.split("/").pop() ?? mid
268
280
  const hasPricing = inputRate > 0 || cacheReadRate > 0
269
281
 
282
+ let trend = 0
283
+ const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0
284
+ if (hasTrendData) {
285
+ trend = lastMsgHitRate - prevMsgHitRate
286
+ }
287
+
288
+ const providerName = pid || ""
289
+
270
290
  return {
271
291
  hitRate, read, write, freshInput: input, output,
272
292
  cost, saved, model, inputRate, cacheReadRate, hasPricing,
273
- hasData: read > 0 || write > 0 || cost > 0,
293
+ hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
294
+ trend, hasTrendData,
295
+ providerName,
274
296
  }
275
297
  })
276
298
 
@@ -301,8 +323,13 @@ function TokenCachePanel(props: {
301
323
  })
302
324
 
303
325
  const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - GUTTER)))
326
+ function trendLabel(t: number): string {
327
+ return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "")
328
+ }
329
+
304
330
  const barW = createMemo(() => {
305
- const overhead = visualWidth(T.hit) + 1 + 2 + 1 + /*pct*/5 + GUTTER
331
+ const trendSpace = d().hasTrendData ? 1 + visualWidth(trendLabel(d().trend)) : 0
332
+ const overhead = visualWidth(T.hit) + 1 + 2 + 1 + /*pct*/5 + trendSpace + GUTTER
306
333
  return Math.max(3, panelWidth() - overhead)
307
334
  })
308
335
  const bar = createMemo(() => progressBar(d().hitRate, barW()))
@@ -337,10 +364,21 @@ function TokenCachePanel(props: {
337
364
  <span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
338
365
  <span style={{ fg: pal().primary }}><b>{T.title}</b></span>
339
366
  <Show when={!open()}>
340
- <span>
341
- {" ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
342
- </span>
343
- <span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
367
+ <Show when={d().hasTrendData}>
368
+ <span>
369
+ {" ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(d().trend))))}
370
+ </span>
371
+ <span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
372
+ <span style={{ fg: d().trend !== 0 ? (d().trend > 0 ? pal().success : pal().error) : pal().text }}>
373
+ {" "}{trendLabel(d().trend)}
374
+ </span>
375
+ </Show>
376
+ <Show when={!d().hasTrendData}>
377
+ <span>
378
+ {" ".repeat(Math.max(1, panelWidth() - GUTTER - 2 - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
379
+ </span>
380
+ <span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
381
+ </Show>
344
382
  </Show>
345
383
  </text>
346
384
 
@@ -352,15 +390,24 @@ function TokenCachePanel(props: {
352
390
  <span style={{ fg: pal().text }}>{T.hit} </span>
353
391
  <span style={{ fg: hitColor() }}>[{bar()}] </span>
354
392
  <span style={{ fg: pal().text }}>{pct()}</span>
393
+ <Show when={d().hasTrendData}>
394
+ <span style={{ fg: d().trend !== 0 ? (d().trend > 0 ? pal().success : pal().error) : pal().text }}>
395
+ {" "}{trendLabel(d().trend)}
396
+ </span>
397
+ </Show>
355
398
  </text>
356
399
 
357
400
  {/* token breakdown */}
358
- <text fg={pal().muted}>
359
- {justify(T.read, fmt(d().read), T.tok)}
360
- </text>
361
- <text fg={pal().muted}>
362
- {justify(T.write, fmt(d().write), T.tok)}
363
- </text>
401
+ <Show when={d().read > 0}>
402
+ <text fg={pal().muted}>
403
+ {justify(T.read, fmt(d().read), T.tok)}
404
+ </text>
405
+ </Show>
406
+ <Show when={d().write > 0}>
407
+ <text fg={pal().muted}>
408
+ {justify(T.write, fmt(d().write), T.tok)}
409
+ </text>
410
+ </Show>
364
411
  <text fg={pal().muted}>
365
412
  {justify(T.miss, fmt(d().freshInput), T.tok)}
366
413
  </text>
@@ -384,6 +431,13 @@ function TokenCachePanel(props: {
384
431
  </text>
385
432
  </Show>
386
433
 
434
+ {/* provider */}
435
+ <Show when={d().providerName}>
436
+ <text fg={pal().muted}>
437
+ {justify(T.provider, d().providerName)}
438
+ </text>
439
+ </Show>
440
+
387
441
  {/* model */}
388
442
  <text fg={pal().muted}>
389
443
  {justify(T.model, d().model)}