opencode-visual-cache 1.2.10 → 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 +17 -4
- package/README_EN.md +17 -4
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +34 -5
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +75 -35
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>
|
|
@@ -164,9 +165,17 @@ rm -rf ~/.cache/opencode/packages/opencode-visual-cache@latest
|
|
|
164
165
|
|
|
165
166
|
---
|
|
166
167
|
|
|
167
|
-
## 6.
|
|
168
|
+
## 6. 语言设置
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
插件支持三种方式控制显示语言,按优先级从高到低排列:
|
|
171
|
+
|
|
172
|
+
### 6.1 运行时切换(推荐)
|
|
173
|
+
|
|
174
|
+
在 TUI 中输入 `/cache-lang`,从弹窗选择「中文」或「English」即可即时切换,无需重启。偏好会自动持久化,下次启动自动恢复。
|
|
175
|
+
|
|
176
|
+
### 6.2 环境变量覆盖
|
|
177
|
+
|
|
178
|
+
启动前设置 `CACHE_TUI_LANG` 环境变量可强制指定语言:
|
|
170
179
|
|
|
171
180
|
```powershell
|
|
172
181
|
# Windows PowerShell
|
|
@@ -178,6 +187,10 @@ $env:CACHE_TUI_LANG="en"; opencode
|
|
|
178
187
|
CACHE_TUI_LANG=en opencode
|
|
179
188
|
```
|
|
180
189
|
|
|
190
|
+
### 6.3 自动检测
|
|
191
|
+
|
|
192
|
+
默认自动检测系统语言。若不符合预期,用 `/cache-lang` 手动切换一次即可,偏好会被记住。
|
|
193
|
+
|
|
181
194
|
---
|
|
182
195
|
|
|
183
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:",
|
|
@@ -295,7 +296,9 @@ function TokenCachePanel(props) {
|
|
|
295
296
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
296
297
|
let boxEl;
|
|
297
298
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
298
|
-
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);
|
|
299
302
|
// ── scan session messages reactively ──
|
|
300
303
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
301
304
|
// api.state.session state changes — no event listener needed.
|
|
@@ -520,6 +523,11 @@ function TokenCachePanel(props) {
|
|
|
520
523
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
521
524
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
522
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
|
+
}
|
|
523
531
|
// Restore distribution snapshot so the token distribution block
|
|
524
532
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
525
533
|
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
@@ -603,7 +611,7 @@ function TokenCachePanel(props) {
|
|
|
603
611
|
}
|
|
604
612
|
const barW = createMemo(() => {
|
|
605
613
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
606
|
-
const overhead = visualWidth(
|
|
614
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
607
615
|
return Math.max(3, panelWidth() - overhead);
|
|
608
616
|
});
|
|
609
617
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
@@ -629,7 +637,7 @@ function TokenCachePanel(props) {
|
|
|
629
637
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
630
638
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
631
639
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
632
|
-
}, 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) })] })] }) })] }) })] }));
|
|
633
641
|
}
|
|
634
642
|
// ---------------------------------------------------------------------------
|
|
635
643
|
// Plugin entry
|
|
@@ -652,9 +660,11 @@ const tui = async (api) => {
|
|
|
652
660
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
653
661
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
654
662
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
663
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
655
664
|
const signals = {
|
|
656
665
|
currencySymbol, setCurrencySymbol,
|
|
657
666
|
exchangeRate, setExchangeRate,
|
|
667
|
+
langZH, setLangZH,
|
|
658
668
|
sectionDetail, setSectionDetail,
|
|
659
669
|
sectionModel, setSectionModel,
|
|
660
670
|
sectionDist, setSectionDist,
|
|
@@ -759,6 +769,25 @@ const tui = async (api) => {
|
|
|
759
769
|
dialog?.clear();
|
|
760
770
|
},
|
|
761
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
|
+
},
|
|
762
791
|
]);
|
|
763
792
|
};
|
|
764
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:",
|
|
@@ -317,6 +319,8 @@ interface PanelSignals {
|
|
|
317
319
|
setCurrencySymbol: (v: string) => void
|
|
318
320
|
exchangeRate: () => number
|
|
319
321
|
setExchangeRate: (v: number) => void
|
|
322
|
+
langZH: () => boolean
|
|
323
|
+
setLangZH: (v: boolean) => void
|
|
320
324
|
sectionDetail: () => boolean
|
|
321
325
|
setSectionDetail: (v: boolean) => void
|
|
322
326
|
sectionModel: () => boolean
|
|
@@ -365,12 +369,16 @@ function TokenCachePanel(props: {
|
|
|
365
369
|
const {
|
|
366
370
|
currencySymbol, setCurrencySymbol,
|
|
367
371
|
exchangeRate, setExchangeRate,
|
|
372
|
+
langZH, setLangZH,
|
|
368
373
|
sectionDetail, setSectionDetail,
|
|
369
374
|
sectionModel, setSectionModel,
|
|
370
375
|
sectionDist, setSectionDist,
|
|
371
376
|
borderVisible, setBorderVisible,
|
|
372
377
|
} = props.signals
|
|
373
378
|
|
|
379
|
+
// ── reactive translation (follows langZH signal) ──
|
|
380
|
+
const t = createMemo(() => langZH() ? ZH_T : EN_T)
|
|
381
|
+
|
|
374
382
|
// ── scan session messages reactively ──
|
|
375
383
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
376
384
|
// api.state.session state changes — no event listener needed.
|
|
@@ -540,6 +548,11 @@ function TokenCachePanel(props: {
|
|
|
540
548
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
541
549
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
542
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
|
+
}
|
|
543
556
|
// Restore distribution snapshot so the token distribution block
|
|
544
557
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
545
558
|
const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
|
|
@@ -623,7 +636,7 @@ function TokenCachePanel(props: {
|
|
|
623
636
|
|
|
624
637
|
const barW = createMemo(() => {
|
|
625
638
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
626
|
-
const overhead = visualWidth(
|
|
639
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
627
640
|
return Math.max(3, panelWidth() - overhead)
|
|
628
641
|
})
|
|
629
642
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
@@ -669,7 +682,7 @@ function TokenCachePanel(props: {
|
|
|
669
682
|
<text onMouseUp={() => setOpen((o) => { const n = !o; persistFold("open", n); return n })}>
|
|
670
683
|
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
671
684
|
<span style={{ fg: pal().primary }}>
|
|
672
|
-
<b>{
|
|
685
|
+
<b>{t().title}</b>
|
|
673
686
|
<Show when={open()}>
|
|
674
687
|
<span style={{ fg: pal().muted }}> (v{PLUGIN_VERSION})</span>
|
|
675
688
|
</Show>
|
|
@@ -677,18 +690,18 @@ function TokenCachePanel(props: {
|
|
|
677
690
|
<Show when={!open() && data().hasData}>
|
|
678
691
|
<Show when={data().hasTrendData}>
|
|
679
692
|
<span>
|
|
680
|
-
{" ".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))))}
|
|
681
694
|
</span>
|
|
682
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
695
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
683
696
|
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
684
697
|
{" "}{trendLabel(data().trend)}
|
|
685
698
|
</span>
|
|
686
699
|
</Show>
|
|
687
700
|
<Show when={!data().hasTrendData}>
|
|
688
701
|
<span>
|
|
689
|
-
{" ".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)))}
|
|
690
703
|
</span>
|
|
691
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
704
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
692
705
|
</Show>
|
|
693
706
|
</Show>
|
|
694
707
|
</text>
|
|
@@ -699,7 +712,7 @@ function TokenCachePanel(props: {
|
|
|
699
712
|
<text fg={pal().muted}>{sep()}</text>
|
|
700
713
|
<text>
|
|
701
714
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
702
|
-
<span style={{ fg: pal().muted }}>{
|
|
715
|
+
<span style={{ fg: pal().muted }}>{t().noData}</span>
|
|
703
716
|
</text>
|
|
704
717
|
</>
|
|
705
718
|
}>
|
|
@@ -707,7 +720,7 @@ function TokenCachePanel(props: {
|
|
|
707
720
|
|
|
708
721
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
709
722
|
<text>
|
|
710
|
-
<span style={{ fg: pal().text }}>{
|
|
723
|
+
<span style={{ fg: pal().text }}>{t().hit} </span>
|
|
711
724
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
712
725
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
713
726
|
<Show when={data().hasTrendData}>
|
|
@@ -719,38 +732,38 @@ function TokenCachePanel(props: {
|
|
|
719
732
|
|
|
720
733
|
{/* session cumulative hit rate */}
|
|
721
734
|
<text fg={pal().muted}>
|
|
722
|
-
{justify(
|
|
735
|
+
{justify(t().totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%")}
|
|
723
736
|
</text>
|
|
724
737
|
|
|
725
738
|
{/* ── detail section (collapsible, default open) ── */}
|
|
726
739
|
<Show when={sectionDetail()}>
|
|
727
740
|
<text onMouseUp={() => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n })}>
|
|
728
741
|
<span style={{ fg: pal().muted }}>{detailOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
729
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
730
|
-
<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>
|
|
731
744
|
</text>
|
|
732
745
|
|
|
733
746
|
<Show when={detailOpen()}>
|
|
734
747
|
<Show when={data().read > 0}>
|
|
735
748
|
<text fg={pal().muted}>
|
|
736
|
-
{justify(
|
|
749
|
+
{justify(t().read, fmt(data().read), t().tok)}
|
|
737
750
|
</text>
|
|
738
751
|
</Show>
|
|
739
752
|
<Show when={data().write > 0}>
|
|
740
753
|
<text fg={pal().muted}>
|
|
741
|
-
{justify(
|
|
754
|
+
{justify(t().write, fmt(data().write), t().tok)}
|
|
742
755
|
</text>
|
|
743
756
|
</Show>
|
|
744
757
|
<text fg={pal().muted}>
|
|
745
|
-
{justify(
|
|
758
|
+
{justify(t().miss, fmt(data().freshInput), t().tok)}
|
|
746
759
|
</text>
|
|
747
760
|
<text fg={pal().muted}>
|
|
748
|
-
{justify(
|
|
761
|
+
{justify(t().out, fmt(data().output), t().tok)}
|
|
749
762
|
</text>
|
|
750
763
|
<Show when={data().saved > 0}>
|
|
751
764
|
<text>
|
|
752
|
-
<span style={{ fg: pal().muted }}>{
|
|
753
|
-
<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>
|
|
754
767
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
755
768
|
</text>
|
|
756
769
|
</Show>
|
|
@@ -761,34 +774,34 @@ function TokenCachePanel(props: {
|
|
|
761
774
|
<Show when={sectionModel()}>
|
|
762
775
|
{<text onMouseUp={() => setModelOpen((o) => { const n = !o; persistFold("model", n); return n })}>
|
|
763
776
|
<span style={{ fg: pal().muted }}>{modelOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
764
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
765
|
-
<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>
|
|
766
779
|
</text>}
|
|
767
780
|
|
|
768
781
|
<Show when={modelOpen()}>
|
|
769
782
|
<text fg={pal().text}>
|
|
770
|
-
{justify(
|
|
783
|
+
{justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate()))}
|
|
771
784
|
</text>
|
|
772
785
|
<Show when={data().providerName}>
|
|
773
786
|
<text fg={pal().muted}>
|
|
774
|
-
{justify(
|
|
787
|
+
{justify(t().provider, data().providerName)}
|
|
775
788
|
</text>
|
|
776
789
|
</Show>
|
|
777
790
|
<text fg={pal().muted}>
|
|
778
|
-
{justify(
|
|
791
|
+
{justify(t().model, data().model)}
|
|
779
792
|
</text>
|
|
780
793
|
<Show when={data().hasPricing}>
|
|
781
794
|
<text fg={pal().muted}>
|
|
782
|
-
{justify(
|
|
795
|
+
{justify(t().rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate)}
|
|
783
796
|
</text>
|
|
784
797
|
<Show when={data().cacheReadRate > 0}>
|
|
785
798
|
<text fg={pal().muted}>
|
|
786
|
-
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " +
|
|
799
|
+
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate)}
|
|
787
800
|
</text>
|
|
788
801
|
</Show>
|
|
789
802
|
<Show when={data().cacheWriteRate > 0}>
|
|
790
803
|
<text fg={pal().muted}>
|
|
791
|
-
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " +
|
|
804
|
+
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate)}
|
|
792
805
|
</text>
|
|
793
806
|
</Show>
|
|
794
807
|
</Show>
|
|
@@ -800,37 +813,37 @@ function TokenCachePanel(props: {
|
|
|
800
813
|
<Show when={data().hasDistData}>
|
|
801
814
|
{<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
|
|
802
815
|
<span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
803
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
804
|
-
<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>
|
|
805
818
|
</text>}
|
|
806
819
|
<Show when={distOpen()}>
|
|
807
820
|
<Show when={data().dist.system > 0}>
|
|
808
821
|
<text fg={pal().muted}>
|
|
809
|
-
{justify(
|
|
822
|
+
{justify(t().distSys, fmt(data().dist.system), t().tok)}
|
|
810
823
|
</text>
|
|
811
824
|
</Show>
|
|
812
825
|
<Show when={data().dist.user > 0}>
|
|
813
826
|
<text fg={pal().muted}>
|
|
814
|
-
{justify(
|
|
827
|
+
{justify(t().distUser, fmt(data().dist.user), t().tok)}
|
|
815
828
|
</text>
|
|
816
829
|
</Show>
|
|
817
830
|
<Show when={data().dist.agent > 0}>
|
|
818
831
|
<text fg={pal().muted}>
|
|
819
|
-
{justify(
|
|
832
|
+
{justify(t().distAgent, fmt(data().dist.agent), t().tok)}
|
|
820
833
|
</text>
|
|
821
834
|
</Show>
|
|
822
835
|
<Show when={data().dist.toolCall > 0}>
|
|
823
836
|
<text fg={pal().muted}>
|
|
824
|
-
{justify(
|
|
837
|
+
{justify(t().distTool, fmt(data().dist.toolCall), t().tok)}
|
|
825
838
|
</text>
|
|
826
839
|
</Show>
|
|
827
840
|
<Show when={data().dist.toolResult > 0}>
|
|
828
841
|
<text fg={pal().muted}>
|
|
829
|
-
{justify(
|
|
842
|
+
{justify(t().distRes, fmt(data().dist.toolResult), t().tok)}
|
|
830
843
|
</text>
|
|
831
844
|
</Show>
|
|
832
845
|
<text fg={pal().text}>
|
|
833
|
-
{justify(
|
|
846
|
+
{justify(t().distTotal, fmt(data().dist.apiInput), t().tok)}
|
|
834
847
|
</text>
|
|
835
848
|
</Show>
|
|
836
849
|
</Show>
|
|
@@ -871,10 +884,12 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
871
884
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
872
885
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
873
886
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
887
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
874
888
|
|
|
875
889
|
const signals: PanelSignals = {
|
|
876
890
|
currencySymbol, setCurrencySymbol,
|
|
877
891
|
exchangeRate, setExchangeRate,
|
|
892
|
+
langZH, setLangZH,
|
|
878
893
|
sectionDetail, setSectionDetail,
|
|
879
894
|
sectionModel, setSectionModel,
|
|
880
895
|
sectionDist, setSectionDist,
|
|
@@ -997,6 +1012,31 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
997
1012
|
dialog?.clear()
|
|
998
1013
|
},
|
|
999
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
|
+
},
|
|
1000
1040
|
])
|
|
1001
1041
|
}
|
|
1002
1042
|
|