opencode-visual-cache 1.2.9 → 1.2.11
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 +19 -4
- package/README_EN.md +17 -4
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +61 -29
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +100 -49
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
- **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
|
|
45
45
|
- **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool 结果)展示估算 Token 占比
|
|
46
46
|
- **折叠记忆**:折叠状态持久化,重启后保持
|
|
47
|
-
-
|
|
47
|
+
- **语言适配**:自动检测系统语言,支持 `/cache-lang` 运行时切换中/英文,偏好持久化
|
|
48
48
|
- **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
|
|
49
|
-
- **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` 动态配置面板
|
|
49
|
+
- **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
@@ -99,6 +99,7 @@ npm install -g opencode-visual-cache@latest
|
|
|
99
99
|
| `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 |
|
|
100
100
|
| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 面板边框的显隐 |
|
|
101
101
|
| `/cache-config` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 |
|
|
102
|
+
| `/cache-lang` | 切换显示语言 | 从列表选择中文或 English,界面即时切换,无需重启 |
|
|
102
103
|
|
|
103
104
|
<div align="center">
|
|
104
105
|
<img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="斜杠命令" width="49%"></img>
|
|
@@ -134,6 +135,8 @@ npm install -g opencode-visual-cache@latest
|
|
|
134
135
|
|
|
135
136
|
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
136
137
|
|
|
138
|
+
> **关于 Token 分布数值**:分布面板中"总计"为最后一次 API 调用的精确 token 数,"系统提示"/"用户"等分项为字符级 BPE 估算值。分项之和通常小于总计,差值主要来自 OpenCode 运行时注入的系统提示组成部分,包括环境信息、Skill 目录、工具 Schema 定义等(详见 [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts)、[`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts))。这些内容不在 agent 配置的 `prompt` 字段中,因此插件无法估算,属于预期行为。
|
|
139
|
+
|
|
137
140
|
---
|
|
138
141
|
|
|
139
142
|
## 5. 更新
|
|
@@ -162,9 +165,17 @@ rm -rf ~/.cache/opencode/packages/opencode-visual-cache@latest
|
|
|
162
165
|
|
|
163
166
|
---
|
|
164
167
|
|
|
165
|
-
## 6.
|
|
168
|
+
## 6. 语言设置
|
|
169
|
+
|
|
170
|
+
插件支持三种方式控制显示语言,按优先级从高到低排列:
|
|
171
|
+
|
|
172
|
+
### 6.1 运行时切换(推荐)
|
|
173
|
+
|
|
174
|
+
在 TUI 中输入 `/cache-lang`,从弹窗选择「中文」或「English」即可即时切换,无需重启。偏好会自动持久化,下次启动自动恢复。
|
|
166
175
|
|
|
167
|
-
|
|
176
|
+
### 6.2 环境变量覆盖
|
|
177
|
+
|
|
178
|
+
启动前设置 `CACHE_TUI_LANG` 环境变量可强制指定语言:
|
|
168
179
|
|
|
169
180
|
```powershell
|
|
170
181
|
# Windows PowerShell
|
|
@@ -176,6 +187,10 @@ $env:CACHE_TUI_LANG="en"; opencode
|
|
|
176
187
|
CACHE_TUI_LANG=en opencode
|
|
177
188
|
```
|
|
178
189
|
|
|
190
|
+
### 6.3 自动检测
|
|
191
|
+
|
|
192
|
+
默认自动检测系统语言。若不符合预期,用 `/cache-lang` 手动切换一次即可,偏好会被记住。
|
|
193
|
+
|
|
179
194
|
---
|
|
180
195
|
|
|
181
196
|
## 7. 兼容性
|
package/README_EN.md
CHANGED
|
@@ -44,9 +44,9 @@ If you find this plugin useful, a ⭐ would mean a lot — thank you!<br>
|
|
|
44
44
|
- **Adaptive Colors**: ≥85% green · ≥70% orange · <70% red, auto-desaturated from current theme
|
|
45
45
|
- **Token Distribution**: Per-role (system / user / agent instr / tool call / tool result) estimated token breakdown
|
|
46
46
|
- **Persistent State**: Fold preferences and config remembered across restarts via api.kv
|
|
47
|
-
- **Language**: Auto-detects system locale
|
|
47
|
+
- **Language**: Auto-detects system locale, with `/cache-lang` for runtime switching between Chinese and English — preference persisted across restarts
|
|
48
48
|
- **Multi-currency**: Switch via `/cache-currency` — costs, savings, and per-million rates convert in real time
|
|
49
|
-
- **Slash Commands**: `/cache-rate` `/cache-section` `/cache-config` for live panel configuration
|
|
49
|
+
- **Slash Commands**: `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` for live panel configuration
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
@@ -99,6 +99,7 @@ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime
|
|
|
99
99
|
| `/cache-rate` | Adjust exchange rate | Enter a custom rate (e.g. `7.2` for CNY) |
|
|
100
100
|
| `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, or the panel border |
|
|
101
101
|
| `/cache-config` | View current config | Displays currency, rate, and section visibility |
|
|
102
|
+
| `/cache-lang` | Switch display language | Pick Chinese or English from the dialog — takes effect immediately, no restart needed |
|
|
102
103
|
|
|
103
104
|
<div align="center">
|
|
104
105
|
<img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="Slash command" width="49%"></img>
|
|
@@ -162,9 +163,17 @@ Press **`Ctrl + P`** in OpenCode → `install plugin` → `opencode-visual-cache
|
|
|
162
163
|
|
|
163
164
|
---
|
|
164
165
|
|
|
165
|
-
## 6.
|
|
166
|
+
## 6. Language Settings
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
The plugin provides three ways to control the display language, listed by priority (highest first):
|
|
169
|
+
|
|
170
|
+
### 6.1 Runtime Switching (recommended)
|
|
171
|
+
|
|
172
|
+
Type `/cache-lang` in the TUI and select **Chinese** or **English** from the dialog. The panel switches immediately without restarting, and your preference is persisted automatically for the next session.
|
|
173
|
+
|
|
174
|
+
### 6.2 Environment Variable Override
|
|
175
|
+
|
|
176
|
+
Set the `CACHE_TUI_LANG` environment variable before launching to force a specific language:
|
|
168
177
|
|
|
169
178
|
```powershell
|
|
170
179
|
# Windows PowerShell
|
|
@@ -176,6 +185,10 @@ $env:CACHE_TUI_LANG="en"; opencode
|
|
|
176
185
|
CACHE_TUI_LANG=en opencode
|
|
177
186
|
```
|
|
178
187
|
|
|
188
|
+
### 6.3 Auto Detection
|
|
189
|
+
|
|
190
|
+
Defaults to the system locale automatically. If it doesn't match, switch once with `/cache-lang` and the preference will be remembered.
|
|
191
|
+
|
|
179
192
|
---
|
|
180
193
|
|
|
181
194
|
## 7. Compatibility
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.2.11";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.2.
|
|
2
|
+
export const PLUGIN_VERSION = "1.2.11";
|
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
50
|
})();
|
|
51
|
-
const
|
|
51
|
+
const ZH_T = {
|
|
52
52
|
title: "缓存统计",
|
|
53
53
|
hit: "命中率",
|
|
54
54
|
totalHit: "总命中:",
|
|
@@ -77,7 +77,8 @@ const T = LANG_ZH ? {
|
|
|
77
77
|
distOut: "输出:",
|
|
78
78
|
secDetail: "明细",
|
|
79
79
|
secModel: "模型",
|
|
80
|
-
}
|
|
80
|
+
};
|
|
81
|
+
const EN_T = {
|
|
81
82
|
title: "Token Cache",
|
|
82
83
|
hit: "Hit",
|
|
83
84
|
totalHit: "Total Hit:",
|
|
@@ -216,7 +217,7 @@ function fmt(n) {
|
|
|
216
217
|
if (n >= 1_000_000)
|
|
217
218
|
return (n / 1_000_000).toFixed(1) + "M";
|
|
218
219
|
if (n >= 10_000)
|
|
219
|
-
return
|
|
220
|
+
return (n / 1_000).toFixed(1) + "K";
|
|
220
221
|
return n.toLocaleString("en-US");
|
|
221
222
|
}
|
|
222
223
|
function num(v) {
|
|
@@ -256,16 +257,19 @@ function estimateTokens(text) {
|
|
|
256
257
|
else
|
|
257
258
|
ascii++;
|
|
258
259
|
}
|
|
259
|
-
//
|
|
260
|
-
// token
|
|
261
|
-
//
|
|
260
|
+
// Real BPE tokenizers (cl100k_base, o200k_base) average ~3.5-4.0
|
|
261
|
+
// ASCII chars/token for both JSON and source code — close to prose.
|
|
262
|
+
// The old 2.0 / 2.5 ratios matched minified-JS extremes, not typical
|
|
263
|
+
// payloads, and systematically over-estimated token counts.
|
|
262
264
|
const trimmed = text.trimStart();
|
|
263
|
-
|
|
265
|
+
// Strip markdown code-fence prefix so that ```json … is detected as JSON
|
|
266
|
+
const strippedFence = trimmed.replace(/^\x60{3}\w*\s*\n?/, "");
|
|
267
|
+
const jsonLike = (strippedFence.startsWith("{") || strippedFence.startsWith("["))
|
|
264
268
|
&& /"[^"]+"\s*:/.test(text);
|
|
265
269
|
const codeLike = !jsonLike
|
|
266
270
|
&& /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text);
|
|
267
|
-
const asciiPerToken = jsonLike ?
|
|
268
|
-
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.
|
|
271
|
+
const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4;
|
|
272
|
+
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
|
|
269
273
|
}
|
|
270
274
|
const CURRENCIES = {
|
|
271
275
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
@@ -292,7 +296,9 @@ function TokenCachePanel(props) {
|
|
|
292
296
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
293
297
|
let boxEl;
|
|
294
298
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
295
|
-
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, borderVisible, setBorderVisible, } = props.signals;
|
|
299
|
+
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, borderVisible, setBorderVisible, } = props.signals;
|
|
300
|
+
// ── reactive translation (follows langZH signal) ──
|
|
301
|
+
const t = createMemo(() => langZH() ? ZH_T : EN_T);
|
|
296
302
|
// ── scan session messages reactively ──
|
|
297
303
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
298
304
|
// api.state.session state changes — no event listener needed.
|
|
@@ -376,6 +382,7 @@ function TokenCachePanel(props) {
|
|
|
376
382
|
const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : "";
|
|
377
383
|
if (sysPrompt)
|
|
378
384
|
dist.system = estimateTokens(sysPrompt);
|
|
385
|
+
let lastAssMsg;
|
|
379
386
|
for (const msg of msgs) {
|
|
380
387
|
if (msg.role === "user") {
|
|
381
388
|
const um = msg;
|
|
@@ -409,14 +416,9 @@ function TokenCachePanel(props) {
|
|
|
409
416
|
const tp = p;
|
|
410
417
|
let rawInput = "";
|
|
411
418
|
try {
|
|
412
|
-
rawInput = tp.state.raw ?? JSON.stringify(tp.state.input);
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
try {
|
|
416
|
-
rawInput = JSON.stringify(tp.state);
|
|
417
|
-
}
|
|
418
|
-
catch { }
|
|
419
|
+
rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "");
|
|
419
420
|
}
|
|
421
|
+
catch { }
|
|
420
422
|
if (rawInput)
|
|
421
423
|
dist.toolCall += estimateTokens(rawInput);
|
|
422
424
|
if (tp.state.status === "completed") {
|
|
@@ -436,19 +438,23 @@ function TokenCachePanel(props) {
|
|
|
436
438
|
const sub = p;
|
|
437
439
|
dist.agent += estimateTokens(sub.prompt || sub.description || "");
|
|
438
440
|
}
|
|
439
|
-
else if (p.type === "step-finish") {
|
|
440
|
-
const sf = p;
|
|
441
|
-
dist.apiInput += sf.tokens?.input ?? 0;
|
|
442
|
-
dist.apiOutput += sf.tokens?.output ?? 0;
|
|
443
|
-
}
|
|
444
441
|
}
|
|
445
442
|
}
|
|
446
443
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
444
|
+
// 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
|
|
445
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
446
|
+
if (msgs[i].role !== "assistant")
|
|
447
|
+
continue;
|
|
448
|
+
const t = msgs[i].tokens;
|
|
449
|
+
if (t && (t.input > 0 || (t.cache?.read ?? 0) > 0)) {
|
|
450
|
+
lastAssMsg = msgs[i];
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// 取最后一条有数据消息的总输入(含缓存读)作为当前 context 大小
|
|
455
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read);
|
|
456
|
+
dist.apiOutput = num(lastAssMsg?.tokens?.output);
|
|
457
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
|
|
452
458
|
}
|
|
453
459
|
catch { }
|
|
454
460
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
@@ -517,6 +523,11 @@ function TokenCachePanel(props) {
|
|
|
517
523
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
518
524
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
519
525
|
setBorderVisible(bv !== false);
|
|
526
|
+
// Restore language preference
|
|
527
|
+
const savedLang = props.api.kv.get(`${KV_PREFIX}.lang`);
|
|
528
|
+
if (savedLang === "zh" || savedLang === "en") {
|
|
529
|
+
setLangZH(savedLang === "zh");
|
|
530
|
+
}
|
|
520
531
|
// Restore distribution snapshot so the token distribution block
|
|
521
532
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
522
533
|
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
@@ -600,7 +611,7 @@ function TokenCachePanel(props) {
|
|
|
600
611
|
}
|
|
601
612
|
const barW = createMemo(() => {
|
|
602
613
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
603
|
-
const overhead = visualWidth(
|
|
614
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
604
615
|
return Math.max(3, panelWidth() - overhead);
|
|
605
616
|
});
|
|
606
617
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
@@ -626,7 +637,7 @@ function TokenCachePanel(props) {
|
|
|
626
637
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
627
638
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
628
639
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
629
|
-
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children:
|
|
640
|
+
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), 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, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secDetail }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t().secDetail)) })] }), _jsxs(Show, { when: detailOpen(), children: [_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(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, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t().secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _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, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t().distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distSys, fmt(data().dist.system), t().tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distUser, fmt(data().dist.user), t().tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distAgent, fmt(data().dist.agent), t().tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distTool, fmt(data().dist.toolCall), t().tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distRes, fmt(data().dist.toolResult), t().tok) }) }), _jsx("text", { fg: pal().text, children: justify(t().distTotal, fmt(data().dist.apiInput), t().tok) })] })] }) })] }) })] }));
|
|
630
641
|
}
|
|
631
642
|
// ---------------------------------------------------------------------------
|
|
632
643
|
// Plugin entry
|
|
@@ -649,9 +660,11 @@ const tui = async (api) => {
|
|
|
649
660
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
650
661
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
651
662
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
663
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
652
664
|
const signals = {
|
|
653
665
|
currencySymbol, setCurrencySymbol,
|
|
654
666
|
exchangeRate, setExchangeRate,
|
|
667
|
+
langZH, setLangZH,
|
|
655
668
|
sectionDetail, setSectionDetail,
|
|
656
669
|
sectionModel, setSectionModel,
|
|
657
670
|
sectionDist, setSectionDist,
|
|
@@ -756,6 +769,25 @@ const tui = async (api) => {
|
|
|
756
769
|
dialog?.clear();
|
|
757
770
|
},
|
|
758
771
|
},
|
|
772
|
+
{
|
|
773
|
+
title: "Cache: Switch Language",
|
|
774
|
+
value: "cache.lang",
|
|
775
|
+
description: "Switch between Chinese and English display",
|
|
776
|
+
slash: { name: "cache-lang" },
|
|
777
|
+
onSelect: (dialog) => {
|
|
778
|
+
const cur = langZH();
|
|
779
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Display Language", options: [
|
|
780
|
+
{ title: `中文 ${cur ? "\u2713" : ""}`, value: "zh" },
|
|
781
|
+
{ title: `English ${cur ? "" : "\u2713"}`, value: "en" },
|
|
782
|
+
], onSelect: (opt) => {
|
|
783
|
+
const zh = opt.value === "zh";
|
|
784
|
+
api.kv.set(`${KV_PREFIX}.lang`, opt.value);
|
|
785
|
+
setLangZH(zh);
|
|
786
|
+
api.ui.toast({ message: zh ? "语言已切换为中文" : "Switched to English" });
|
|
787
|
+
dialog?.clear();
|
|
788
|
+
} })));
|
|
789
|
+
},
|
|
790
|
+
},
|
|
759
791
|
]);
|
|
760
792
|
};
|
|
761
793
|
const mod = {
|
package/package.json
CHANGED
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.2.
|
|
2
|
+
export const PLUGIN_VERSION="1.2.11";
|
package/src/index.tsx
CHANGED
|
@@ -71,7 +71,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
71
71
|
catch { return false }
|
|
72
72
|
})()
|
|
73
73
|
|
|
74
|
-
const
|
|
74
|
+
const ZH_T = {
|
|
75
75
|
title: "缓存统计",
|
|
76
76
|
hit: "命中率",
|
|
77
77
|
totalHit: "总命中:",
|
|
@@ -100,7 +100,9 @@ const T = LANG_ZH ? {
|
|
|
100
100
|
distOut: "输出:",
|
|
101
101
|
secDetail: "明细",
|
|
102
102
|
secModel: "模型",
|
|
103
|
-
} as const
|
|
103
|
+
} as const
|
|
104
|
+
|
|
105
|
+
const EN_T = {
|
|
104
106
|
title: "Token Cache",
|
|
105
107
|
hit: "Hit",
|
|
106
108
|
totalHit: "Total Hit:",
|
|
@@ -241,7 +243,7 @@ function progressBar(percent: number, width: number): string {
|
|
|
241
243
|
|
|
242
244
|
function fmt(n: number): string {
|
|
243
245
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
|
|
244
|
-
if (n >= 10_000) return
|
|
246
|
+
if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
|
|
245
247
|
return n.toLocaleString("en-US")
|
|
246
248
|
}
|
|
247
249
|
|
|
@@ -277,17 +279,20 @@ function estimateTokens(text: string): number {
|
|
|
277
279
|
else ascii++
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
//
|
|
281
|
-
// token
|
|
282
|
-
//
|
|
282
|
+
// Real BPE tokenizers (cl100k_base, o200k_base) average ~3.5-4.0
|
|
283
|
+
// ASCII chars/token for both JSON and source code — close to prose.
|
|
284
|
+
// The old 2.0 / 2.5 ratios matched minified-JS extremes, not typical
|
|
285
|
+
// payloads, and systematically over-estimated token counts.
|
|
283
286
|
const trimmed = text.trimStart()
|
|
284
|
-
|
|
287
|
+
// Strip markdown code-fence prefix so that ```json … is detected as JSON
|
|
288
|
+
const strippedFence = trimmed.replace(/^\x60{3}\w*\s*\n?/, "")
|
|
289
|
+
const jsonLike = (strippedFence.startsWith("{") || strippedFence.startsWith("["))
|
|
285
290
|
&& /"[^"]+"\s*:/.test(text)
|
|
286
291
|
const codeLike = !jsonLike
|
|
287
292
|
&& /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text)
|
|
288
293
|
|
|
289
|
-
const asciiPerToken = jsonLike ?
|
|
290
|
-
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.
|
|
294
|
+
const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4
|
|
295
|
+
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0))
|
|
291
296
|
}
|
|
292
297
|
|
|
293
298
|
interface TokenDist {
|
|
@@ -314,6 +319,8 @@ interface PanelSignals {
|
|
|
314
319
|
setCurrencySymbol: (v: string) => void
|
|
315
320
|
exchangeRate: () => number
|
|
316
321
|
setExchangeRate: (v: number) => void
|
|
322
|
+
langZH: () => boolean
|
|
323
|
+
setLangZH: (v: boolean) => void
|
|
317
324
|
sectionDetail: () => boolean
|
|
318
325
|
setSectionDetail: (v: boolean) => void
|
|
319
326
|
sectionModel: () => boolean
|
|
@@ -362,12 +369,16 @@ function TokenCachePanel(props: {
|
|
|
362
369
|
const {
|
|
363
370
|
currencySymbol, setCurrencySymbol,
|
|
364
371
|
exchangeRate, setExchangeRate,
|
|
372
|
+
langZH, setLangZH,
|
|
365
373
|
sectionDetail, setSectionDetail,
|
|
366
374
|
sectionModel, setSectionModel,
|
|
367
375
|
sectionDist, setSectionDist,
|
|
368
376
|
borderVisible, setBorderVisible,
|
|
369
377
|
} = props.signals
|
|
370
378
|
|
|
379
|
+
// ── reactive translation (follows langZH signal) ──
|
|
380
|
+
const t = createMemo(() => langZH() ? ZH_T : EN_T)
|
|
381
|
+
|
|
371
382
|
// ── scan session messages reactively ──
|
|
372
383
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
373
384
|
// api.state.session state changes — no event listener needed.
|
|
@@ -436,6 +447,7 @@ function TokenCachePanel(props: {
|
|
|
436
447
|
const agentCfg = agents?.[agentName] as Record<string, unknown> | undefined
|
|
437
448
|
const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : ""
|
|
438
449
|
if (sysPrompt) dist.system = estimateTokens(sysPrompt)
|
|
450
|
+
let lastAssMsg: AssistantMessage | undefined
|
|
439
451
|
for (const msg of msgs) {
|
|
440
452
|
if (msg.role === "user") {
|
|
441
453
|
const um = msg as UserMessage; if (um.system) dist.system += estimateTokens(um.system)
|
|
@@ -445,24 +457,31 @@ function TokenCachePanel(props: {
|
|
|
445
457
|
else if (p.type === "file") { const fp = p as any; if (fp.source?.text?.value) dist.user += estimateTokens(fp.source.text.value) }
|
|
446
458
|
}
|
|
447
459
|
} else if (msg.role === "assistant") {
|
|
448
|
-
const am = msg as AssistantMessage
|
|
460
|
+
const am = msg as AssistantMessage
|
|
461
|
+
dist.output += num(am.tokens?.output)
|
|
449
462
|
let parts: readonly Part[] = []; try { parts = props.api.state.part(msg.id) } catch {}
|
|
450
463
|
for (const p of parts) {
|
|
451
464
|
if (p.type === "tool") {
|
|
452
465
|
const tp = p as any; let rawInput = ""
|
|
453
|
-
try { rawInput = tp.state.raw ??
|
|
466
|
+
try { rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "") } catch {}
|
|
454
467
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
455
468
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
456
469
|
else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
|
|
457
470
|
} else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
|
|
458
471
|
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
459
|
-
else if (p.type === "step-finish") { const sf = p as any; dist.apiInput += sf.tokens?.input ?? 0; dist.apiOutput += sf.tokens?.output ?? 0 }
|
|
460
472
|
}
|
|
461
473
|
}
|
|
462
474
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
475
|
+
// 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
|
|
476
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
477
|
+
if (msgs[i].role !== "assistant") continue
|
|
478
|
+
const t = (msgs[i] as AssistantMessage).tokens
|
|
479
|
+
if (t && (t.input > 0 || (t.cache?.read ?? 0) > 0)) { lastAssMsg = msgs[i] as AssistantMessage; break }
|
|
480
|
+
}
|
|
481
|
+
// 取最后一条有数据消息的总输入(含缓存读)作为当前 context 大小
|
|
482
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read)
|
|
483
|
+
dist.apiOutput = num(lastAssMsg?.tokens?.output)
|
|
484
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
|
|
466
485
|
} catch {}
|
|
467
486
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
468
487
|
return { finalDist, finalHasDist }
|
|
@@ -479,7 +498,7 @@ function TokenCachePanel(props: {
|
|
|
479
498
|
|
|
480
499
|
const data = createMemo(() => {
|
|
481
500
|
return dataSignal()
|
|
482
|
-
})
|
|
501
|
+
})
|
|
483
502
|
|
|
484
503
|
// Persist the last valid distribution so that data() can fall back
|
|
485
504
|
// to it while api.state.part() is re-hydrating after a view switch.
|
|
@@ -529,6 +548,11 @@ function TokenCachePanel(props: {
|
|
|
529
548
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
530
549
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
531
550
|
setBorderVisible(bv !== false)
|
|
551
|
+
// Restore language preference
|
|
552
|
+
const savedLang = props.api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
553
|
+
if (savedLang === "zh" || savedLang === "en") {
|
|
554
|
+
setLangZH(savedLang === "zh")
|
|
555
|
+
}
|
|
532
556
|
// Restore distribution snapshot so the token distribution block
|
|
533
557
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
534
558
|
const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
|
|
@@ -612,7 +636,7 @@ function TokenCachePanel(props: {
|
|
|
612
636
|
|
|
613
637
|
const barW = createMemo(() => {
|
|
614
638
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
615
|
-
const overhead = visualWidth(
|
|
639
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
616
640
|
return Math.max(3, panelWidth() - overhead)
|
|
617
641
|
})
|
|
618
642
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
@@ -658,7 +682,7 @@ function TokenCachePanel(props: {
|
|
|
658
682
|
<text onMouseUp={() => setOpen((o) => { const n = !o; persistFold("open", n); return n })}>
|
|
659
683
|
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
660
684
|
<span style={{ fg: pal().primary }}>
|
|
661
|
-
<b>{
|
|
685
|
+
<b>{t().title}</b>
|
|
662
686
|
<Show when={open()}>
|
|
663
687
|
<span style={{ fg: pal().muted }}> (v{PLUGIN_VERSION})</span>
|
|
664
688
|
</Show>
|
|
@@ -666,18 +690,18 @@ function TokenCachePanel(props: {
|
|
|
666
690
|
<Show when={!open() && data().hasData}>
|
|
667
691
|
<Show when={data().hasTrendData}>
|
|
668
692
|
<span>
|
|
669
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(
|
|
693
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded + " " + trendLabel(data().trend))))}
|
|
670
694
|
</span>
|
|
671
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
695
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
672
696
|
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
673
697
|
{" "}{trendLabel(data().trend)}
|
|
674
698
|
</span>
|
|
675
699
|
</Show>
|
|
676
700
|
<Show when={!data().hasTrendData}>
|
|
677
701
|
<span>
|
|
678
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(
|
|
702
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded)))}
|
|
679
703
|
</span>
|
|
680
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
704
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
681
705
|
</Show>
|
|
682
706
|
</Show>
|
|
683
707
|
</text>
|
|
@@ -688,7 +712,7 @@ function TokenCachePanel(props: {
|
|
|
688
712
|
<text fg={pal().muted}>{sep()}</text>
|
|
689
713
|
<text>
|
|
690
714
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
691
|
-
<span style={{ fg: pal().muted }}>{
|
|
715
|
+
<span style={{ fg: pal().muted }}>{t().noData}</span>
|
|
692
716
|
</text>
|
|
693
717
|
</>
|
|
694
718
|
}>
|
|
@@ -696,7 +720,7 @@ function TokenCachePanel(props: {
|
|
|
696
720
|
|
|
697
721
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
698
722
|
<text>
|
|
699
|
-
<span style={{ fg: pal().text }}>{
|
|
723
|
+
<span style={{ fg: pal().text }}>{t().hit} </span>
|
|
700
724
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
701
725
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
702
726
|
<Show when={data().hasTrendData}>
|
|
@@ -708,38 +732,38 @@ function TokenCachePanel(props: {
|
|
|
708
732
|
|
|
709
733
|
{/* session cumulative hit rate */}
|
|
710
734
|
<text fg={pal().muted}>
|
|
711
|
-
{justify(
|
|
735
|
+
{justify(t().totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%")}
|
|
712
736
|
</text>
|
|
713
737
|
|
|
714
738
|
{/* ── detail section (collapsible, default open) ── */}
|
|
715
739
|
<Show when={sectionDetail()}>
|
|
716
740
|
<text onMouseUp={() => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n })}>
|
|
717
741
|
<span style={{ fg: pal().muted }}>{detailOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
718
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
719
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") +
|
|
742
|
+
<span style={{ fg: pal().primary }}><b>{t().secDetail}</b></span>
|
|
743
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t().secDetail))}</span>
|
|
720
744
|
</text>
|
|
721
745
|
|
|
722
746
|
<Show when={detailOpen()}>
|
|
723
747
|
<Show when={data().read > 0}>
|
|
724
748
|
<text fg={pal().muted}>
|
|
725
|
-
{justify(
|
|
749
|
+
{justify(t().read, fmt(data().read), t().tok)}
|
|
726
750
|
</text>
|
|
727
751
|
</Show>
|
|
728
752
|
<Show when={data().write > 0}>
|
|
729
753
|
<text fg={pal().muted}>
|
|
730
|
-
{justify(
|
|
754
|
+
{justify(t().write, fmt(data().write), t().tok)}
|
|
731
755
|
</text>
|
|
732
756
|
</Show>
|
|
733
757
|
<text fg={pal().muted}>
|
|
734
|
-
{justify(
|
|
758
|
+
{justify(t().miss, fmt(data().freshInput), t().tok)}
|
|
735
759
|
</text>
|
|
736
760
|
<text fg={pal().muted}>
|
|
737
|
-
{justify(
|
|
761
|
+
{justify(t().out, fmt(data().output), t().tok)}
|
|
738
762
|
</text>
|
|
739
763
|
<Show when={data().saved > 0}>
|
|
740
764
|
<text>
|
|
741
|
-
<span style={{ fg: pal().muted }}>{
|
|
742
|
-
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(
|
|
765
|
+
<span style={{ fg: pal().muted }}>{t().saved}</span>
|
|
766
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t().saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
|
|
743
767
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
744
768
|
</text>
|
|
745
769
|
</Show>
|
|
@@ -750,34 +774,34 @@ function TokenCachePanel(props: {
|
|
|
750
774
|
<Show when={sectionModel()}>
|
|
751
775
|
{<text onMouseUp={() => setModelOpen((o) => { const n = !o; persistFold("model", n); return n })}>
|
|
752
776
|
<span style={{ fg: pal().muted }}>{modelOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
753
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
754
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") +
|
|
777
|
+
<span style={{ fg: pal().primary }}><b>{t().secModel}</b></span>
|
|
778
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t().secModel))}</span>
|
|
755
779
|
</text>}
|
|
756
780
|
|
|
757
781
|
<Show when={modelOpen()}>
|
|
758
782
|
<text fg={pal().text}>
|
|
759
|
-
{justify(
|
|
783
|
+
{justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate()))}
|
|
760
784
|
</text>
|
|
761
785
|
<Show when={data().providerName}>
|
|
762
786
|
<text fg={pal().muted}>
|
|
763
|
-
{justify(
|
|
787
|
+
{justify(t().provider, data().providerName)}
|
|
764
788
|
</text>
|
|
765
789
|
</Show>
|
|
766
790
|
<text fg={pal().muted}>
|
|
767
|
-
{justify(
|
|
791
|
+
{justify(t().model, data().model)}
|
|
768
792
|
</text>
|
|
769
793
|
<Show when={data().hasPricing}>
|
|
770
794
|
<text fg={pal().muted}>
|
|
771
|
-
{justify(
|
|
795
|
+
{justify(t().rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate)}
|
|
772
796
|
</text>
|
|
773
797
|
<Show when={data().cacheReadRate > 0}>
|
|
774
798
|
<text fg={pal().muted}>
|
|
775
|
-
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " +
|
|
799
|
+
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate)}
|
|
776
800
|
</text>
|
|
777
801
|
</Show>
|
|
778
802
|
<Show when={data().cacheWriteRate > 0}>
|
|
779
803
|
<text fg={pal().muted}>
|
|
780
|
-
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " +
|
|
804
|
+
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate)}
|
|
781
805
|
</text>
|
|
782
806
|
</Show>
|
|
783
807
|
</Show>
|
|
@@ -789,37 +813,37 @@ function TokenCachePanel(props: {
|
|
|
789
813
|
<Show when={data().hasDistData}>
|
|
790
814
|
{<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
|
|
791
815
|
<span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
792
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
793
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") +
|
|
816
|
+
<span style={{ fg: pal().primary }}><b>{t().distTitle}</b></span>
|
|
817
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t().distTitle))}</span>
|
|
794
818
|
</text>}
|
|
795
819
|
<Show when={distOpen()}>
|
|
796
820
|
<Show when={data().dist.system > 0}>
|
|
797
821
|
<text fg={pal().muted}>
|
|
798
|
-
{justify(
|
|
822
|
+
{justify(t().distSys, fmt(data().dist.system), t().tok)}
|
|
799
823
|
</text>
|
|
800
824
|
</Show>
|
|
801
825
|
<Show when={data().dist.user > 0}>
|
|
802
826
|
<text fg={pal().muted}>
|
|
803
|
-
{justify(
|
|
827
|
+
{justify(t().distUser, fmt(data().dist.user), t().tok)}
|
|
804
828
|
</text>
|
|
805
829
|
</Show>
|
|
806
830
|
<Show when={data().dist.agent > 0}>
|
|
807
831
|
<text fg={pal().muted}>
|
|
808
|
-
{justify(
|
|
832
|
+
{justify(t().distAgent, fmt(data().dist.agent), t().tok)}
|
|
809
833
|
</text>
|
|
810
834
|
</Show>
|
|
811
835
|
<Show when={data().dist.toolCall > 0}>
|
|
812
836
|
<text fg={pal().muted}>
|
|
813
|
-
{justify(
|
|
837
|
+
{justify(t().distTool, fmt(data().dist.toolCall), t().tok)}
|
|
814
838
|
</text>
|
|
815
839
|
</Show>
|
|
816
840
|
<Show when={data().dist.toolResult > 0}>
|
|
817
841
|
<text fg={pal().muted}>
|
|
818
|
-
{justify(
|
|
842
|
+
{justify(t().distRes, fmt(data().dist.toolResult), t().tok)}
|
|
819
843
|
</text>
|
|
820
844
|
</Show>
|
|
821
845
|
<text fg={pal().text}>
|
|
822
|
-
{justify(
|
|
846
|
+
{justify(t().distTotal, fmt(data().dist.apiInput), t().tok)}
|
|
823
847
|
</text>
|
|
824
848
|
</Show>
|
|
825
849
|
</Show>
|
|
@@ -860,10 +884,12 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
860
884
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
861
885
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
862
886
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
887
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
863
888
|
|
|
864
889
|
const signals: PanelSignals = {
|
|
865
890
|
currencySymbol, setCurrencySymbol,
|
|
866
891
|
exchangeRate, setExchangeRate,
|
|
892
|
+
langZH, setLangZH,
|
|
867
893
|
sectionDetail, setSectionDetail,
|
|
868
894
|
sectionModel, setSectionModel,
|
|
869
895
|
sectionDist, setSectionDist,
|
|
@@ -986,6 +1012,31 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
986
1012
|
dialog?.clear()
|
|
987
1013
|
},
|
|
988
1014
|
},
|
|
1015
|
+
{
|
|
1016
|
+
title: "Cache: Switch Language",
|
|
1017
|
+
value: "cache.lang",
|
|
1018
|
+
description: "Switch between Chinese and English display",
|
|
1019
|
+
slash: { name: "cache-lang" },
|
|
1020
|
+
onSelect: (dialog) => {
|
|
1021
|
+
const cur = langZH()
|
|
1022
|
+
dialog?.replace(() => (
|
|
1023
|
+
<api.ui.DialogSelect
|
|
1024
|
+
title="Display Language"
|
|
1025
|
+
options={[
|
|
1026
|
+
{ title: `中文 ${cur ? "\u2713" : ""}`, value: "zh" },
|
|
1027
|
+
{ title: `English ${cur ? "" : "\u2713"}`, value: "en" },
|
|
1028
|
+
]}
|
|
1029
|
+
onSelect={(opt) => {
|
|
1030
|
+
const zh = opt.value === "zh"
|
|
1031
|
+
api.kv.set(`${KV_PREFIX}.lang`, opt.value)
|
|
1032
|
+
setLangZH(zh)
|
|
1033
|
+
api.ui.toast({ message: zh ? "语言已切换为中文" : "Switched to English" })
|
|
1034
|
+
dialog?.clear()
|
|
1035
|
+
}}
|
|
1036
|
+
/>
|
|
1037
|
+
))
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
989
1040
|
])
|
|
990
1041
|
}
|
|
991
1042
|
|