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 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. Debug
166
+ ## 6. Language Settings
166
167
 
167
- Force English:
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
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.10";
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.10";
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 T = LANG_ZH ? {
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(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
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: 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) })] })] }) })] }) })] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.2.10";
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 T = LANG_ZH ? {
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(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
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>{T.title}</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(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
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()} {T.hitFolded}</span>
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(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
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()} {T.hitFolded}</span>
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 }}>{T.noData}</span>
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 }}>{T.hit} </span>
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(T.totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%")}
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>{T.secDetail}</b></span>
730
- <span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + T.secDetail))}</span>
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(T.read, fmt(data().read), T.tok)}
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(T.write, fmt(data().write), T.tok)}
754
+ {justify(t().write, fmt(data().write), t().tok)}
742
755
  </text>
743
756
  </Show>
744
757
  <text fg={pal().muted}>
745
- {justify(T.miss, fmt(data().freshInput), T.tok)}
758
+ {justify(t().miss, fmt(data().freshInput), t().tok)}
746
759
  </text>
747
760
  <text fg={pal().muted}>
748
- {justify(T.out, fmt(data().output), T.tok)}
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 }}>{T.saved}</span>
753
- <span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
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>{T.secModel}</b></span>
765
- <span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + T.secModel))}</span>
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(T.cost, fmtCost(data().cost, currencySymbol(), exchangeRate()))}
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(T.provider, data().providerName)}
787
+ {justify(t().provider, data().providerName)}
775
788
  </text>
776
789
  </Show>
777
790
  <text fg={pal().muted}>
778
- {justify(T.model, data().model)}
791
+ {justify(t().model, data().model)}
779
792
  </text>
780
793
  <Show when={data().hasPricing}>
781
794
  <text fg={pal().muted}>
782
- {justify(T.rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + T.inputRate)}
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 " + T.cacheRate)}
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 " + T.writeRate)}
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>{T.distTitle}</b></span>
804
- <span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + T.distTitle))}</span>
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(T.distSys, fmt(data().dist.system), T.tok)}
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(T.distUser, fmt(data().dist.user), T.tok)}
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(T.distAgent, fmt(data().dist.agent), T.tok)}
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(T.distTool, fmt(data().dist.toolCall), T.tok)}
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(T.distRes, fmt(data().dist.toolResult), T.tok)}
842
+ {justify(t().distRes, fmt(data().dist.toolResult), t().tok)}
830
843
  </text>
831
844
  </Show>
832
845
  <text fg={pal().text}>
833
- {justify(T.distTotal, fmt(data().dist.apiInput), T.tok)}
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