opencode-visual-cache 1.0.6 → 1.1.0
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/README.md +1 -1
- package/README_EN.md +1 -1
- package/dist/index.js +35 -15
- package/package.json +1 -1
- package/src/index.tsx +108 -70
package/README.md
CHANGED
package/README_EN.md
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
### Option 1: OpenCode Command (recommended)
|
|
45
45
|
|
|
46
|
-
Press **`
|
|
46
|
+
Press **`Ctrl + P`** in OpenCode to open the command palette, search **`install plugin`**, then type:
|
|
47
47
|
|
|
48
48
|
```
|
|
49
49
|
opencode-visual-cache
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
|
|
2
2
|
import { createMemo, createSignal, Show } from "solid-js";
|
|
3
3
|
// ── terminal-width helpers ────────────────────────────────────────
|
|
4
4
|
// CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
@@ -50,6 +50,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
50
50
|
const T = LANG_ZH ? {
|
|
51
51
|
title: "缓存统计",
|
|
52
52
|
hit: "命中率",
|
|
53
|
+
totalHit: "总命中:",
|
|
53
54
|
read: "缓存读:",
|
|
54
55
|
write: "缓存写:",
|
|
55
56
|
miss: "未命中:",
|
|
@@ -62,10 +63,13 @@ const T = LANG_ZH ? {
|
|
|
62
63
|
hitFolded: "命中",
|
|
63
64
|
inputRate: "输入",
|
|
64
65
|
cacheRate: "缓存",
|
|
66
|
+
writeRate: "写入",
|
|
67
|
+
noData: "等待缓存数据...",
|
|
65
68
|
tok: "tok",
|
|
66
69
|
} : {
|
|
67
70
|
title: "Token Cache",
|
|
68
71
|
hit: "Hit",
|
|
72
|
+
totalHit: "Total Hit:",
|
|
69
73
|
read: "Read:",
|
|
70
74
|
write: "Write:",
|
|
71
75
|
miss: "Miss:",
|
|
@@ -78,6 +82,8 @@ const T = LANG_ZH ? {
|
|
|
78
82
|
hitFolded: "hit",
|
|
79
83
|
inputRate: "in",
|
|
80
84
|
cacheRate: "cache",
|
|
85
|
+
writeRate: "write",
|
|
86
|
+
noData: "Waiting for cache data...",
|
|
81
87
|
tok: "tok",
|
|
82
88
|
};
|
|
83
89
|
// ── color helpers ────────────────────────────────────────────────
|
|
@@ -188,9 +194,17 @@ const MIN_PANEL_WIDTH = 20;
|
|
|
188
194
|
const DEFAULT_PANEL_WIDTH = 26;
|
|
189
195
|
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
190
196
|
const GUTTER = 2 + 4;
|
|
197
|
+
/** ── layout measurement constants (visual columns) ── */
|
|
198
|
+
const LABEL_GAP = 1; // label(如 "Hit")后面的空格
|
|
199
|
+
const BAR_BRACKETS = 2; // "[" + "]" 包围进度条
|
|
200
|
+
const BAR_GAP = 1; // "]" 后面的空格
|
|
201
|
+
const PCT_FIXED_WIDTH = 5; // "XX.X%" 固定 5 字符宽度
|
|
202
|
+
const HEADER_PREFIX = 2; // 折叠态标题行:▶/▼ 图标 + 后面的空格
|
|
203
|
+
const UNIT_GAP = 1; // 计量单位前的空格(如 "tok")
|
|
191
204
|
function TokenCachePanel(props) {
|
|
192
205
|
const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH);
|
|
193
206
|
const [open, setOpen] = createSignal(true);
|
|
207
|
+
let boxEl;
|
|
194
208
|
// ── scan session messages reactively ──
|
|
195
209
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
196
210
|
// api.state.session state changes — no event listener needed.
|
|
@@ -210,6 +224,8 @@ function TokenCachePanel(props) {
|
|
|
210
224
|
if (msg.role !== "assistant")
|
|
211
225
|
continue;
|
|
212
226
|
const t = msg.tokens;
|
|
227
|
+
if (!t)
|
|
228
|
+
continue;
|
|
213
229
|
const msgInputTokens = num(t.input) + num(t.cache?.read);
|
|
214
230
|
const msgReadTokens = num(t.cache?.read);
|
|
215
231
|
if (msgInputTokens > 0) {
|
|
@@ -230,6 +246,7 @@ function TokenCachePanel(props) {
|
|
|
230
246
|
let saved = 0;
|
|
231
247
|
let inputRate = 0;
|
|
232
248
|
let cacheReadRate = 0;
|
|
249
|
+
let cacheWriteRate = 0;
|
|
233
250
|
if (read > 0 && pid && mid) {
|
|
234
251
|
for (const provider of props.api.state.provider) {
|
|
235
252
|
if (provider.id !== pid)
|
|
@@ -239,6 +256,7 @@ function TokenCachePanel(props) {
|
|
|
239
256
|
continue;
|
|
240
257
|
inputRate = num(model.cost.input);
|
|
241
258
|
cacheReadRate = num(model.cost.cache?.read);
|
|
259
|
+
cacheWriteRate = num(model.cost.cache?.write);
|
|
242
260
|
const diff = inputRate - cacheReadRate;
|
|
243
261
|
if (diff > 0)
|
|
244
262
|
saved = (read * diff) / 1_000_000;
|
|
@@ -246,11 +264,12 @@ function TokenCachePanel(props) {
|
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
// `input` from the API represents fresh (non-cached) tokens.
|
|
267
|
+
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0;
|
|
249
268
|
// Total context = fresh + cache.read.
|
|
250
269
|
const totalInput = input + read;
|
|
251
|
-
const
|
|
270
|
+
const sessionHitRate = totalInput > 0 ? (read / totalInput) * 100 : 0;
|
|
252
271
|
const model = mid.split("/").pop() ?? mid;
|
|
253
|
-
const hasPricing = inputRate > 0 || cacheReadRate > 0;
|
|
272
|
+
const hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
|
|
254
273
|
let trend = 0;
|
|
255
274
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
|
|
256
275
|
if (hasTrendData) {
|
|
@@ -259,13 +278,13 @@ function TokenCachePanel(props) {
|
|
|
259
278
|
const providerName = pid || "";
|
|
260
279
|
return {
|
|
261
280
|
hitRate, read, write, freshInput: input, output,
|
|
262
|
-
cost, saved, model, inputRate, cacheReadRate, hasPricing,
|
|
281
|
+
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
263
282
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
264
283
|
trend, hasTrendData,
|
|
265
284
|
providerName,
|
|
285
|
+
sessionHitRate,
|
|
266
286
|
};
|
|
267
287
|
});
|
|
268
|
-
const d = () => data();
|
|
269
288
|
// ── colours ──
|
|
270
289
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
271
290
|
// fall back to Morandi when a key is missing from the theme.
|
|
@@ -283,7 +302,7 @@ function TokenCachePanel(props) {
|
|
|
283
302
|
};
|
|
284
303
|
});
|
|
285
304
|
const hitColor = createMemo(() => {
|
|
286
|
-
const r =
|
|
305
|
+
const r = data().hitRate;
|
|
287
306
|
if (r >= 85)
|
|
288
307
|
return pal().success;
|
|
289
308
|
if (r >= 70)
|
|
@@ -295,23 +314,24 @@ function TokenCachePanel(props) {
|
|
|
295
314
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "");
|
|
296
315
|
}
|
|
297
316
|
const barW = createMemo(() => {
|
|
298
|
-
const trendSpace =
|
|
299
|
-
const overhead = visualWidth(T.hit) +
|
|
317
|
+
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
318
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + GUTTER;
|
|
300
319
|
return Math.max(3, panelWidth() - overhead);
|
|
301
320
|
});
|
|
302
|
-
const bar = createMemo(() => progressBar(
|
|
303
|
-
const pct = createMemo(() =>
|
|
321
|
+
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
322
|
+
const pct = createMemo(() => data().hitRate.toFixed(1) + "%");
|
|
304
323
|
// left-align label, right-align value — auto-fill space between
|
|
305
324
|
const justify = (label, value, unit = "") => {
|
|
306
325
|
const gauge = panelWidth() - GUTTER;
|
|
307
|
-
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) +
|
|
326
|
+
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0);
|
|
308
327
|
const gap = Math.max(1, gauge - used);
|
|
309
328
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
310
329
|
};
|
|
311
|
-
return (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
return (_jsxs("box", { border: true, borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: 2, paddingRight: 2, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
331
|
+
// 通过 ref 获取 Renderable 实例宽度
|
|
332
|
+
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width) : DEFAULT_PANEL_WIDTH;
|
|
333
|
+
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
334
|
+
}, 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() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] })] }), _jsx(Show, { when: open(), children: _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: T.noData })] })] }), 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: data().hasTrendData, children: _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(T.totalHit, data().sessionHitRate.toFixed(1) + "%") }), _jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(data().read), T.tok) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(data().write), T.tok) }) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(data().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(data().output), T.tok) }), _jsx("text", { fg: pal().muted, children: sep() }), _jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(data().cost)) }), _jsx(Show, { when: data().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(data().saved)))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved)] })] }) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, "$" + data().inputRate.toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + data().cacheReadRate.toFixed(2) + "/M " + T.cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + data().cacheWriteRate.toFixed(2) + "/M " + T.writeRate) }) })] })] }) })] }));
|
|
315
335
|
}
|
|
316
336
|
// ---------------------------------------------------------------------------
|
|
317
337
|
// Plugin entry
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -66,6 +66,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
66
66
|
const T = LANG_ZH ? {
|
|
67
67
|
title: "缓存统计",
|
|
68
68
|
hit: "命中率",
|
|
69
|
+
totalHit: "总命中:",
|
|
69
70
|
read: "缓存读:",
|
|
70
71
|
write: "缓存写:",
|
|
71
72
|
miss: "未命中:",
|
|
@@ -78,10 +79,13 @@ const T = LANG_ZH ? {
|
|
|
78
79
|
hitFolded: "命中",
|
|
79
80
|
inputRate: "输入",
|
|
80
81
|
cacheRate: "缓存",
|
|
82
|
+
writeRate: "写入",
|
|
83
|
+
noData: "等待缓存数据...",
|
|
81
84
|
tok: "tok",
|
|
82
85
|
} as const : {
|
|
83
86
|
title: "Token Cache",
|
|
84
87
|
hit: "Hit",
|
|
88
|
+
totalHit: "Total Hit:",
|
|
85
89
|
read: "Read:",
|
|
86
90
|
write: "Write:",
|
|
87
91
|
miss: "Miss:",
|
|
@@ -94,6 +98,8 @@ const T = LANG_ZH ? {
|
|
|
94
98
|
hitFolded: "hit",
|
|
95
99
|
inputRate: "in",
|
|
96
100
|
cacheRate: "cache",
|
|
101
|
+
writeRate: "write",
|
|
102
|
+
noData: "Waiting for cache data...",
|
|
97
103
|
tok: "tok",
|
|
98
104
|
} as const
|
|
99
105
|
|
|
@@ -209,6 +215,14 @@ const DEFAULT_PANEL_WIDTH = 26
|
|
|
209
215
|
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
210
216
|
const GUTTER = 2 + 4
|
|
211
217
|
|
|
218
|
+
/** ── layout measurement constants (visual columns) ── */
|
|
219
|
+
const LABEL_GAP = 1 // label(如 "Hit")后面的空格
|
|
220
|
+
const BAR_BRACKETS = 2 // "[" + "]" 包围进度条
|
|
221
|
+
const BAR_GAP = 1 // "]" 后面的空格
|
|
222
|
+
const PCT_FIXED_WIDTH = 5 // "XX.X%" 固定 5 字符宽度
|
|
223
|
+
const HEADER_PREFIX = 2 // 折叠态标题行:▶/▼ 图标 + 后面的空格
|
|
224
|
+
const UNIT_GAP = 1 // 计量单位前的空格(如 "tok")
|
|
225
|
+
|
|
212
226
|
function TokenCachePanel(props: {
|
|
213
227
|
theme: TuiThemeCurrent
|
|
214
228
|
api: TuiPluginApi
|
|
@@ -216,6 +230,7 @@ function TokenCachePanel(props: {
|
|
|
216
230
|
}): JSX.Element {
|
|
217
231
|
const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH)
|
|
218
232
|
const [open, setOpen] = createSignal(true)
|
|
233
|
+
let boxEl: any
|
|
219
234
|
|
|
220
235
|
// ── scan session messages reactively ──
|
|
221
236
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
@@ -238,6 +253,7 @@ function TokenCachePanel(props: {
|
|
|
238
253
|
for (const msg of msgs) {
|
|
239
254
|
if (msg.role !== "assistant") continue
|
|
240
255
|
const t = (msg as AssistantMessage).tokens
|
|
256
|
+
if (!t) continue
|
|
241
257
|
const msgInputTokens = num(t.input) + num(t.cache?.read)
|
|
242
258
|
const msgReadTokens = num(t.cache?.read)
|
|
243
259
|
if (msgInputTokens > 0) {
|
|
@@ -259,6 +275,7 @@ function TokenCachePanel(props: {
|
|
|
259
275
|
let saved = 0
|
|
260
276
|
let inputRate = 0
|
|
261
277
|
let cacheReadRate = 0
|
|
278
|
+
let cacheWriteRate = 0
|
|
262
279
|
if (read > 0 && pid && mid) {
|
|
263
280
|
for (const provider of props.api.state.provider) {
|
|
264
281
|
if (provider.id !== pid) continue
|
|
@@ -266,6 +283,7 @@ function TokenCachePanel(props: {
|
|
|
266
283
|
if (!model?.cost) continue
|
|
267
284
|
inputRate = num(model.cost.input)
|
|
268
285
|
cacheReadRate = num(model.cost.cache?.read)
|
|
286
|
+
cacheWriteRate = num(model.cost.cache?.write)
|
|
269
287
|
const diff = inputRate - cacheReadRate
|
|
270
288
|
if (diff > 0) saved = (read * diff) / 1_000_000
|
|
271
289
|
break
|
|
@@ -273,11 +291,12 @@ function TokenCachePanel(props: {
|
|
|
273
291
|
}
|
|
274
292
|
|
|
275
293
|
// `input` from the API represents fresh (non-cached) tokens.
|
|
294
|
+
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0
|
|
276
295
|
// Total context = fresh + cache.read.
|
|
277
296
|
const totalInput = input + read
|
|
278
|
-
const
|
|
297
|
+
const sessionHitRate = totalInput > 0 ? (read / totalInput) * 100 : 0
|
|
279
298
|
const model = mid.split("/").pop() ?? mid
|
|
280
|
-
const hasPricing = inputRate > 0 || cacheReadRate > 0
|
|
299
|
+
const hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0
|
|
281
300
|
|
|
282
301
|
let trend = 0
|
|
283
302
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0
|
|
@@ -289,15 +308,14 @@ function TokenCachePanel(props: {
|
|
|
289
308
|
|
|
290
309
|
return {
|
|
291
310
|
hitRate, read, write, freshInput: input, output,
|
|
292
|
-
cost, saved, model, inputRate, cacheReadRate, hasPricing,
|
|
311
|
+
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
293
312
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
294
313
|
trend, hasTrendData,
|
|
295
314
|
providerName,
|
|
315
|
+
sessionHitRate,
|
|
296
316
|
}
|
|
297
317
|
})
|
|
298
318
|
|
|
299
|
-
const d = () => data()
|
|
300
|
-
|
|
301
319
|
// ── colours ──
|
|
302
320
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
303
321
|
// fall back to Morandi when a key is missing from the theme.
|
|
@@ -316,7 +334,7 @@ function TokenCachePanel(props: {
|
|
|
316
334
|
})
|
|
317
335
|
|
|
318
336
|
const hitColor = createMemo(() => {
|
|
319
|
-
const r =
|
|
337
|
+
const r = data().hitRate
|
|
320
338
|
if (r >= 85) return pal().success
|
|
321
339
|
if (r >= 70) return pal().warning
|
|
322
340
|
return pal().error
|
|
@@ -328,61 +346,71 @@ function TokenCachePanel(props: {
|
|
|
328
346
|
}
|
|
329
347
|
|
|
330
348
|
const barW = createMemo(() => {
|
|
331
|
-
const trendSpace =
|
|
332
|
-
const overhead = visualWidth(T.hit) +
|
|
349
|
+
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
350
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + GUTTER
|
|
333
351
|
return Math.max(3, panelWidth() - overhead)
|
|
334
352
|
})
|
|
335
|
-
const bar = createMemo(() => progressBar(
|
|
336
|
-
const pct = createMemo(() =>
|
|
353
|
+
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
354
|
+
const pct = createMemo(() => data().hitRate.toFixed(1) + "%")
|
|
337
355
|
|
|
338
356
|
// left-align label, right-align value — auto-fill space between
|
|
339
357
|
const justify = (label: string, value: string, unit = ""): string => {
|
|
340
358
|
const gauge = panelWidth() - GUTTER
|
|
341
|
-
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) +
|
|
359
|
+
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
|
|
342
360
|
const gap = Math.max(1, gauge - used)
|
|
343
361
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "")
|
|
344
362
|
}
|
|
345
363
|
|
|
346
364
|
return (
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
</
|
|
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>
|
|
365
|
+
<box
|
|
366
|
+
border
|
|
367
|
+
borderColor={pal().border}
|
|
368
|
+
paddingTop={0}
|
|
369
|
+
paddingBottom={0}
|
|
370
|
+
paddingLeft={2}
|
|
371
|
+
paddingRight={2}
|
|
372
|
+
flexDirection="column"
|
|
373
|
+
gap={0}
|
|
374
|
+
ref={boxEl}
|
|
375
|
+
onSizeChange={() => {
|
|
376
|
+
// 通过 ref 获取 Renderable 实例宽度
|
|
377
|
+
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width) : DEFAULT_PANEL_WIDTH
|
|
378
|
+
setPanelWidth((prev) => (prev === w ? prev : w))
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
{/* collapsible header */}
|
|
382
|
+
<text onMouseUp={() => setOpen((o) => !o)}>
|
|
383
|
+
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
384
|
+
<span style={{ fg: pal().primary }}><b>{T.title}</b></span>
|
|
385
|
+
<Show when={!open() && data().hasData}>
|
|
386
|
+
<Show when={data().hasTrendData}>
|
|
387
|
+
<span>
|
|
388
|
+
{" ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
|
|
389
|
+
</span>
|
|
390
|
+
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
391
|
+
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
392
|
+
{" "}{trendLabel(data().trend)}
|
|
393
|
+
</span>
|
|
382
394
|
</Show>
|
|
383
|
-
|
|
395
|
+
<Show when={!data().hasTrendData}>
|
|
396
|
+
<span>
|
|
397
|
+
{" ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
|
|
398
|
+
</span>
|
|
399
|
+
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
400
|
+
</Show>
|
|
401
|
+
</Show>
|
|
402
|
+
</text>
|
|
384
403
|
|
|
385
|
-
|
|
404
|
+
<Show when={open()}>
|
|
405
|
+
<Show when={data().hasData} fallback={
|
|
406
|
+
<>
|
|
407
|
+
<text fg={pal().muted}>{sep()}</text>
|
|
408
|
+
<text>
|
|
409
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
410
|
+
<span style={{ fg: pal().muted }}>{T.noData}</span>
|
|
411
|
+
</text>
|
|
412
|
+
</>
|
|
413
|
+
}>
|
|
386
414
|
<text fg={pal().muted}>{sep()}</text>
|
|
387
415
|
|
|
388
416
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
@@ -390,73 +418,83 @@ function TokenCachePanel(props: {
|
|
|
390
418
|
<span style={{ fg: pal().text }}>{T.hit} </span>
|
|
391
419
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
392
420
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
393
|
-
<Show when={
|
|
394
|
-
<span style={{ fg:
|
|
395
|
-
{" "}{trendLabel(
|
|
421
|
+
<Show when={data().hasTrendData}>
|
|
422
|
+
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
423
|
+
{" "}{trendLabel(data().trend)}
|
|
396
424
|
</span>
|
|
397
425
|
</Show>
|
|
398
426
|
</text>
|
|
399
427
|
|
|
428
|
+
{/* session cumulative hit rate */}
|
|
429
|
+
<text fg={pal().muted}>
|
|
430
|
+
{justify(T.totalHit, data().sessionHitRate.toFixed(1) + "%")}
|
|
431
|
+
</text>
|
|
432
|
+
|
|
400
433
|
{/* token breakdown */}
|
|
401
|
-
<Show when={
|
|
434
|
+
<Show when={data().read > 0}>
|
|
402
435
|
<text fg={pal().muted}>
|
|
403
|
-
{justify(T.read, fmt(
|
|
436
|
+
{justify(T.read, fmt(data().read), T.tok)}
|
|
404
437
|
</text>
|
|
405
438
|
</Show>
|
|
406
|
-
<Show when={
|
|
439
|
+
<Show when={data().write > 0}>
|
|
407
440
|
<text fg={pal().muted}>
|
|
408
|
-
{justify(T.write, fmt(
|
|
441
|
+
{justify(T.write, fmt(data().write), T.tok)}
|
|
409
442
|
</text>
|
|
410
443
|
</Show>
|
|
411
444
|
<text fg={pal().muted}>
|
|
412
|
-
{justify(T.miss, fmt(
|
|
445
|
+
{justify(T.miss, fmt(data().freshInput), T.tok)}
|
|
413
446
|
</text>
|
|
414
447
|
<text fg={pal().muted}>
|
|
415
|
-
{justify(T.out, fmt(
|
|
448
|
+
{justify(T.out, fmt(data().output), T.tok)}
|
|
416
449
|
</text>
|
|
417
450
|
|
|
418
451
|
<text fg={pal().muted}>{sep()}</text>
|
|
419
452
|
|
|
420
453
|
{/* cost */}
|
|
421
454
|
<text fg={pal().text}>
|
|
422
|
-
{justify(T.cost, fmtCost(
|
|
455
|
+
{justify(T.cost, fmtCost(data().cost))}
|
|
423
456
|
</text>
|
|
424
457
|
|
|
425
458
|
{/* saved */}
|
|
426
|
-
<Show when={
|
|
459
|
+
<Show when={data().saved > 0}>
|
|
427
460
|
<text>
|
|
428
461
|
<span style={{ fg: pal().muted }}>{T.saved}</span>
|
|
429
|
-
<span>{" ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(
|
|
430
|
-
<span style={{ fg: pal().success }}>~{fmtCost(
|
|
462
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved))))}</span>
|
|
463
|
+
<span style={{ fg: pal().success }}>~{fmtCost(data().saved)}</span>
|
|
431
464
|
</text>
|
|
432
465
|
</Show>
|
|
433
466
|
|
|
434
467
|
{/* provider */}
|
|
435
|
-
<Show when={
|
|
468
|
+
<Show when={data().providerName}>
|
|
436
469
|
<text fg={pal().muted}>
|
|
437
|
-
{justify(T.provider,
|
|
470
|
+
{justify(T.provider, data().providerName)}
|
|
438
471
|
</text>
|
|
439
472
|
</Show>
|
|
440
473
|
|
|
441
474
|
{/* model */}
|
|
442
475
|
<text fg={pal().muted}>
|
|
443
|
-
{justify(T.model,
|
|
476
|
+
{justify(T.model, data().model)}
|
|
444
477
|
</text>
|
|
445
478
|
|
|
446
479
|
{/* rates */}
|
|
447
|
-
<Show when={
|
|
480
|
+
<Show when={data().hasPricing}>
|
|
448
481
|
<text fg={pal().muted}>
|
|
449
|
-
{justify(T.rate, "$" +
|
|
482
|
+
{justify(T.rate, "$" + data().inputRate.toFixed(2) + "/M " + T.inputRate)}
|
|
450
483
|
</text>
|
|
451
|
-
<Show when={
|
|
484
|
+
<Show when={data().cacheReadRate > 0}>
|
|
485
|
+
<text fg={pal().muted}>
|
|
486
|
+
{justify("", "$" + data().cacheReadRate.toFixed(2) + "/M " + T.cacheRate)}
|
|
487
|
+
</text>
|
|
488
|
+
</Show>
|
|
489
|
+
<Show when={data().cacheWriteRate > 0}>
|
|
452
490
|
<text fg={pal().muted}>
|
|
453
|
-
{justify("", "$" +
|
|
491
|
+
{justify("", "$" + data().cacheWriteRate.toFixed(2) + "/M " + T.writeRate)}
|
|
454
492
|
</text>
|
|
455
493
|
</Show>
|
|
456
494
|
</Show>
|
|
457
495
|
</Show>
|
|
458
|
-
</
|
|
459
|
-
</
|
|
496
|
+
</Show>
|
|
497
|
+
</box>
|
|
460
498
|
)
|
|
461
499
|
}
|
|
462
500
|
|