opencode-visual-cache 1.0.7 → 1.1.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/dist/index.js +37 -15
- package/package.json +1 -1
- package/src/index.tsx +115 -69
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,19 @@ 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")
|
|
204
|
+
/** Current plugin version — bump alongside npm version. */
|
|
205
|
+
const PLUGIN_VERSION = '1.1.1';
|
|
191
206
|
function TokenCachePanel(props) {
|
|
192
207
|
const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH);
|
|
193
208
|
const [open, setOpen] = createSignal(true);
|
|
209
|
+
let boxEl;
|
|
194
210
|
// ── scan session messages reactively ──
|
|
195
211
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
196
212
|
// api.state.session state changes — no event listener needed.
|
|
@@ -210,6 +226,8 @@ function TokenCachePanel(props) {
|
|
|
210
226
|
if (msg.role !== "assistant")
|
|
211
227
|
continue;
|
|
212
228
|
const t = msg.tokens;
|
|
229
|
+
if (!t)
|
|
230
|
+
continue;
|
|
213
231
|
const msgInputTokens = num(t.input) + num(t.cache?.read);
|
|
214
232
|
const msgReadTokens = num(t.cache?.read);
|
|
215
233
|
if (msgInputTokens > 0) {
|
|
@@ -230,6 +248,7 @@ function TokenCachePanel(props) {
|
|
|
230
248
|
let saved = 0;
|
|
231
249
|
let inputRate = 0;
|
|
232
250
|
let cacheReadRate = 0;
|
|
251
|
+
let cacheWriteRate = 0;
|
|
233
252
|
if (read > 0 && pid && mid) {
|
|
234
253
|
for (const provider of props.api.state.provider) {
|
|
235
254
|
if (provider.id !== pid)
|
|
@@ -239,6 +258,7 @@ function TokenCachePanel(props) {
|
|
|
239
258
|
continue;
|
|
240
259
|
inputRate = num(model.cost.input);
|
|
241
260
|
cacheReadRate = num(model.cost.cache?.read);
|
|
261
|
+
cacheWriteRate = num(model.cost.cache?.write);
|
|
242
262
|
const diff = inputRate - cacheReadRate;
|
|
243
263
|
if (diff > 0)
|
|
244
264
|
saved = (read * diff) / 1_000_000;
|
|
@@ -246,11 +266,12 @@ function TokenCachePanel(props) {
|
|
|
246
266
|
}
|
|
247
267
|
}
|
|
248
268
|
// `input` from the API represents fresh (non-cached) tokens.
|
|
269
|
+
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0;
|
|
249
270
|
// Total context = fresh + cache.read.
|
|
250
271
|
const totalInput = input + read;
|
|
251
|
-
const
|
|
272
|
+
const sessionHitRate = totalInput > 0 ? (read / totalInput) * 100 : 0;
|
|
252
273
|
const model = mid.split("/").pop() ?? mid;
|
|
253
|
-
const hasPricing = inputRate > 0 || cacheReadRate > 0;
|
|
274
|
+
const hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
|
|
254
275
|
let trend = 0;
|
|
255
276
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
|
|
256
277
|
if (hasTrendData) {
|
|
@@ -259,13 +280,13 @@ function TokenCachePanel(props) {
|
|
|
259
280
|
const providerName = pid || "";
|
|
260
281
|
return {
|
|
261
282
|
hitRate, read, write, freshInput: input, output,
|
|
262
|
-
cost, saved, model, inputRate, cacheReadRate, hasPricing,
|
|
283
|
+
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
263
284
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
264
285
|
trend, hasTrendData,
|
|
265
286
|
providerName,
|
|
287
|
+
sessionHitRate,
|
|
266
288
|
};
|
|
267
289
|
});
|
|
268
|
-
const d = () => data();
|
|
269
290
|
// ── colours ──
|
|
270
291
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
271
292
|
// fall back to Morandi when a key is missing from the theme.
|
|
@@ -283,7 +304,7 @@ function TokenCachePanel(props) {
|
|
|
283
304
|
};
|
|
284
305
|
});
|
|
285
306
|
const hitColor = createMemo(() => {
|
|
286
|
-
const r =
|
|
307
|
+
const r = data().hitRate;
|
|
287
308
|
if (r >= 85)
|
|
288
309
|
return pal().success;
|
|
289
310
|
if (r >= 70)
|
|
@@ -295,23 +316,24 @@ function TokenCachePanel(props) {
|
|
|
295
316
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "");
|
|
296
317
|
}
|
|
297
318
|
const barW = createMemo(() => {
|
|
298
|
-
const trendSpace =
|
|
299
|
-
const overhead = visualWidth(T.hit) +
|
|
319
|
+
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
320
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + GUTTER;
|
|
300
321
|
return Math.max(3, panelWidth() - overhead);
|
|
301
322
|
});
|
|
302
|
-
const bar = createMemo(() => progressBar(
|
|
303
|
-
const pct = createMemo(() =>
|
|
323
|
+
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
324
|
+
const pct = createMemo(() => data().hitRate.toFixed(1) + "%");
|
|
304
325
|
// left-align label, right-align value — auto-fill space between
|
|
305
326
|
const justify = (label, value, unit = "") => {
|
|
306
327
|
const gauge = panelWidth() - GUTTER;
|
|
307
|
-
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) +
|
|
328
|
+
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0);
|
|
308
329
|
const gap = Math.max(1, gauge - used);
|
|
309
330
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
310
331
|
};
|
|
311
|
-
return (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
return (_jsxs("box", { border: true, borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: 2, paddingRight: 2, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
333
|
+
// 通过 ref 获取 Renderable 实例宽度
|
|
334
|
+
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width) : DEFAULT_PANEL_WIDTH;
|
|
335
|
+
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
336
|
+
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => !o), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: T.title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: pal().muted }, children: [" (v", PLUGIN_VERSION, ")"] }) })] }), _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
337
|
}
|
|
316
338
|
// ---------------------------------------------------------------------------
|
|
317
339
|
// 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,17 @@ 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
|
+
|
|
226
|
+
/** Current plugin version — bump alongside npm version. */
|
|
227
|
+
const PLUGIN_VERSION = '1.1.1'
|
|
228
|
+
|
|
212
229
|
function TokenCachePanel(props: {
|
|
213
230
|
theme: TuiThemeCurrent
|
|
214
231
|
api: TuiPluginApi
|
|
@@ -216,6 +233,7 @@ function TokenCachePanel(props: {
|
|
|
216
233
|
}): JSX.Element {
|
|
217
234
|
const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH)
|
|
218
235
|
const [open, setOpen] = createSignal(true)
|
|
236
|
+
let boxEl: any
|
|
219
237
|
|
|
220
238
|
// ── scan session messages reactively ──
|
|
221
239
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
@@ -238,6 +256,7 @@ function TokenCachePanel(props: {
|
|
|
238
256
|
for (const msg of msgs) {
|
|
239
257
|
if (msg.role !== "assistant") continue
|
|
240
258
|
const t = (msg as AssistantMessage).tokens
|
|
259
|
+
if (!t) continue
|
|
241
260
|
const msgInputTokens = num(t.input) + num(t.cache?.read)
|
|
242
261
|
const msgReadTokens = num(t.cache?.read)
|
|
243
262
|
if (msgInputTokens > 0) {
|
|
@@ -259,6 +278,7 @@ function TokenCachePanel(props: {
|
|
|
259
278
|
let saved = 0
|
|
260
279
|
let inputRate = 0
|
|
261
280
|
let cacheReadRate = 0
|
|
281
|
+
let cacheWriteRate = 0
|
|
262
282
|
if (read > 0 && pid && mid) {
|
|
263
283
|
for (const provider of props.api.state.provider) {
|
|
264
284
|
if (provider.id !== pid) continue
|
|
@@ -266,6 +286,7 @@ function TokenCachePanel(props: {
|
|
|
266
286
|
if (!model?.cost) continue
|
|
267
287
|
inputRate = num(model.cost.input)
|
|
268
288
|
cacheReadRate = num(model.cost.cache?.read)
|
|
289
|
+
cacheWriteRate = num(model.cost.cache?.write)
|
|
269
290
|
const diff = inputRate - cacheReadRate
|
|
270
291
|
if (diff > 0) saved = (read * diff) / 1_000_000
|
|
271
292
|
break
|
|
@@ -273,11 +294,12 @@ function TokenCachePanel(props: {
|
|
|
273
294
|
}
|
|
274
295
|
|
|
275
296
|
// `input` from the API represents fresh (non-cached) tokens.
|
|
297
|
+
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0
|
|
276
298
|
// Total context = fresh + cache.read.
|
|
277
299
|
const totalInput = input + read
|
|
278
|
-
const
|
|
300
|
+
const sessionHitRate = totalInput > 0 ? (read / totalInput) * 100 : 0
|
|
279
301
|
const model = mid.split("/").pop() ?? mid
|
|
280
|
-
const hasPricing = inputRate > 0 || cacheReadRate > 0
|
|
302
|
+
const hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0
|
|
281
303
|
|
|
282
304
|
let trend = 0
|
|
283
305
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0
|
|
@@ -289,15 +311,14 @@ function TokenCachePanel(props: {
|
|
|
289
311
|
|
|
290
312
|
return {
|
|
291
313
|
hitRate, read, write, freshInput: input, output,
|
|
292
|
-
cost, saved, model, inputRate, cacheReadRate, hasPricing,
|
|
314
|
+
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
293
315
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
294
316
|
trend, hasTrendData,
|
|
295
317
|
providerName,
|
|
318
|
+
sessionHitRate,
|
|
296
319
|
}
|
|
297
320
|
})
|
|
298
321
|
|
|
299
|
-
const d = () => data()
|
|
300
|
-
|
|
301
322
|
// ── colours ──
|
|
302
323
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
303
324
|
// fall back to Morandi when a key is missing from the theme.
|
|
@@ -316,7 +337,7 @@ function TokenCachePanel(props: {
|
|
|
316
337
|
})
|
|
317
338
|
|
|
318
339
|
const hitColor = createMemo(() => {
|
|
319
|
-
const r =
|
|
340
|
+
const r = data().hitRate
|
|
320
341
|
if (r >= 85) return pal().success
|
|
321
342
|
if (r >= 70) return pal().warning
|
|
322
343
|
return pal().error
|
|
@@ -328,61 +349,76 @@ function TokenCachePanel(props: {
|
|
|
328
349
|
}
|
|
329
350
|
|
|
330
351
|
const barW = createMemo(() => {
|
|
331
|
-
const trendSpace =
|
|
332
|
-
const overhead = visualWidth(T.hit) +
|
|
352
|
+
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
353
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + GUTTER
|
|
333
354
|
return Math.max(3, panelWidth() - overhead)
|
|
334
355
|
})
|
|
335
|
-
const bar = createMemo(() => progressBar(
|
|
336
|
-
const pct = createMemo(() =>
|
|
356
|
+
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
357
|
+
const pct = createMemo(() => data().hitRate.toFixed(1) + "%")
|
|
337
358
|
|
|
338
359
|
// left-align label, right-align value — auto-fill space between
|
|
339
360
|
const justify = (label: string, value: string, unit = ""): string => {
|
|
340
361
|
const gauge = panelWidth() - GUTTER
|
|
341
|
-
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) +
|
|
362
|
+
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
|
|
342
363
|
const gap = Math.max(1, gauge - used)
|
|
343
364
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "")
|
|
344
365
|
}
|
|
345
366
|
|
|
346
367
|
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
|
-
</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>
|
|
368
|
+
<box
|
|
369
|
+
border
|
|
370
|
+
borderColor={pal().border}
|
|
371
|
+
paddingTop={0}
|
|
372
|
+
paddingBottom={0}
|
|
373
|
+
paddingLeft={2}
|
|
374
|
+
paddingRight={2}
|
|
375
|
+
flexDirection="column"
|
|
376
|
+
gap={0}
|
|
377
|
+
ref={boxEl}
|
|
378
|
+
onSizeChange={() => {
|
|
379
|
+
// 通过 ref 获取 Renderable 实例宽度
|
|
380
|
+
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width) : DEFAULT_PANEL_WIDTH
|
|
381
|
+
setPanelWidth((prev) => (prev === w ? prev : w))
|
|
382
|
+
}}
|
|
383
|
+
>
|
|
384
|
+
{/* collapsible header */}
|
|
385
|
+
<text onMouseUp={() => setOpen((o) => !o)}>
|
|
386
|
+
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
387
|
+
<span style={{ fg: pal().primary }}>
|
|
388
|
+
<b>{T.title}</b>
|
|
389
|
+
<Show when={open()}>
|
|
390
|
+
<span style={{ fg: pal().muted }}> (v{PLUGIN_VERSION})</span>
|
|
381
391
|
</Show>
|
|
392
|
+
</span>
|
|
393
|
+
<Show when={!open() && data().hasData}>
|
|
394
|
+
<Show when={data().hasTrendData}>
|
|
395
|
+
<span>
|
|
396
|
+
{" ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
|
|
397
|
+
</span>
|
|
398
|
+
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
399
|
+
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
400
|
+
{" "}{trendLabel(data().trend)}
|
|
401
|
+
</span>
|
|
382
402
|
</Show>
|
|
383
|
-
|
|
403
|
+
<Show when={!data().hasTrendData}>
|
|
404
|
+
<span>
|
|
405
|
+
{" ".repeat(Math.max(1, panelWidth() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
|
|
406
|
+
</span>
|
|
407
|
+
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
408
|
+
</Show>
|
|
409
|
+
</Show>
|
|
410
|
+
</text>
|
|
384
411
|
|
|
385
|
-
|
|
412
|
+
<Show when={open()}>
|
|
413
|
+
<Show when={data().hasData} fallback={
|
|
414
|
+
<>
|
|
415
|
+
<text fg={pal().muted}>{sep()}</text>
|
|
416
|
+
<text>
|
|
417
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
418
|
+
<span style={{ fg: pal().muted }}>{T.noData}</span>
|
|
419
|
+
</text>
|
|
420
|
+
</>
|
|
421
|
+
}>
|
|
386
422
|
<text fg={pal().muted}>{sep()}</text>
|
|
387
423
|
|
|
388
424
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
@@ -390,73 +426,83 @@ function TokenCachePanel(props: {
|
|
|
390
426
|
<span style={{ fg: pal().text }}>{T.hit} </span>
|
|
391
427
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
392
428
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
393
|
-
<Show when={
|
|
394
|
-
<span style={{ fg:
|
|
395
|
-
{" "}{trendLabel(
|
|
429
|
+
<Show when={data().hasTrendData}>
|
|
430
|
+
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
431
|
+
{" "}{trendLabel(data().trend)}
|
|
396
432
|
</span>
|
|
397
433
|
</Show>
|
|
398
434
|
</text>
|
|
399
435
|
|
|
436
|
+
{/* session cumulative hit rate */}
|
|
437
|
+
<text fg={pal().muted}>
|
|
438
|
+
{justify(T.totalHit, data().sessionHitRate.toFixed(1) + "%")}
|
|
439
|
+
</text>
|
|
440
|
+
|
|
400
441
|
{/* token breakdown */}
|
|
401
|
-
<Show when={
|
|
442
|
+
<Show when={data().read > 0}>
|
|
402
443
|
<text fg={pal().muted}>
|
|
403
|
-
{justify(T.read, fmt(
|
|
444
|
+
{justify(T.read, fmt(data().read), T.tok)}
|
|
404
445
|
</text>
|
|
405
446
|
</Show>
|
|
406
|
-
<Show when={
|
|
447
|
+
<Show when={data().write > 0}>
|
|
407
448
|
<text fg={pal().muted}>
|
|
408
|
-
{justify(T.write, fmt(
|
|
449
|
+
{justify(T.write, fmt(data().write), T.tok)}
|
|
409
450
|
</text>
|
|
410
451
|
</Show>
|
|
411
452
|
<text fg={pal().muted}>
|
|
412
|
-
{justify(T.miss, fmt(
|
|
453
|
+
{justify(T.miss, fmt(data().freshInput), T.tok)}
|
|
413
454
|
</text>
|
|
414
455
|
<text fg={pal().muted}>
|
|
415
|
-
{justify(T.out, fmt(
|
|
456
|
+
{justify(T.out, fmt(data().output), T.tok)}
|
|
416
457
|
</text>
|
|
417
458
|
|
|
418
459
|
<text fg={pal().muted}>{sep()}</text>
|
|
419
460
|
|
|
420
461
|
{/* cost */}
|
|
421
462
|
<text fg={pal().text}>
|
|
422
|
-
{justify(T.cost, fmtCost(
|
|
463
|
+
{justify(T.cost, fmtCost(data().cost))}
|
|
423
464
|
</text>
|
|
424
465
|
|
|
425
466
|
{/* saved */}
|
|
426
|
-
<Show when={
|
|
467
|
+
<Show when={data().saved > 0}>
|
|
427
468
|
<text>
|
|
428
469
|
<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(
|
|
470
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved))))}</span>
|
|
471
|
+
<span style={{ fg: pal().success }}>~{fmtCost(data().saved)}</span>
|
|
431
472
|
</text>
|
|
432
473
|
</Show>
|
|
433
474
|
|
|
434
475
|
{/* provider */}
|
|
435
|
-
<Show when={
|
|
476
|
+
<Show when={data().providerName}>
|
|
436
477
|
<text fg={pal().muted}>
|
|
437
|
-
{justify(T.provider,
|
|
478
|
+
{justify(T.provider, data().providerName)}
|
|
438
479
|
</text>
|
|
439
480
|
</Show>
|
|
440
481
|
|
|
441
482
|
{/* model */}
|
|
442
483
|
<text fg={pal().muted}>
|
|
443
|
-
{justify(T.model,
|
|
484
|
+
{justify(T.model, data().model)}
|
|
444
485
|
</text>
|
|
445
486
|
|
|
446
487
|
{/* rates */}
|
|
447
|
-
<Show when={
|
|
488
|
+
<Show when={data().hasPricing}>
|
|
448
489
|
<text fg={pal().muted}>
|
|
449
|
-
{justify(T.rate, "$" +
|
|
490
|
+
{justify(T.rate, "$" + data().inputRate.toFixed(2) + "/M " + T.inputRate)}
|
|
450
491
|
</text>
|
|
451
|
-
<Show when={
|
|
492
|
+
<Show when={data().cacheReadRate > 0}>
|
|
493
|
+
<text fg={pal().muted}>
|
|
494
|
+
{justify("", "$" + data().cacheReadRate.toFixed(2) + "/M " + T.cacheRate)}
|
|
495
|
+
</text>
|
|
496
|
+
</Show>
|
|
497
|
+
<Show when={data().cacheWriteRate > 0}>
|
|
452
498
|
<text fg={pal().muted}>
|
|
453
|
-
{justify("", "$" +
|
|
499
|
+
{justify("", "$" + data().cacheWriteRate.toFixed(2) + "/M " + T.writeRate)}
|
|
454
500
|
</text>
|
|
455
501
|
</Show>
|
|
456
502
|
</Show>
|
|
457
503
|
</Show>
|
|
458
|
-
</
|
|
459
|
-
</
|
|
504
|
+
</Show>
|
|
505
|
+
</box>
|
|
460
506
|
)
|
|
461
507
|
}
|
|
462
508
|
|