opencode-visual-cache 1.2.4 → 1.2.6

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
@@ -19,7 +19,7 @@
19
19
 
20
20
  ---
21
21
 
22
- ## 图片展示
22
+ ## 1. 图片展示
23
23
 
24
24
  <div align="center">
25
25
  <strong>支持折叠,节省侧边栏占用👇</strong> <br>
@@ -34,24 +34,26 @@
34
34
 
35
35
 
36
36
  ---
37
- ## 功能
37
+ ## 2. 功能
38
38
 
39
39
  - **缓存命中率**:实时计算并显示缓存命中率,自适应宽度进度条
40
40
  - **Token 明细**:缓存读 / 缓存写 / 未命中 / 输出,标签左对齐 · 数据右对齐
41
41
  - **费用与节省**:Session 累计费用 + 缓存命中带来的费用节省
42
- - **模型定价**:显示当前模型的输入 / 缓存读单价(从 provider 配置动态读取)
43
- - **折叠面板**:点击标题折叠为一行,节省侧边栏空间
42
+ - **模型定价**:显示当前模型的输入 / 缓存读 / 缓存写单价(从 provider 配置动态读取)
43
+ - **折叠面板**:主标题默认折叠,点击展开;明细、模型、分布各自独立折叠
44
44
  - **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
45
- - **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool 结果)展示可解析 Token 占比
45
+ - **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool 结果)展示估算 Token 占比
46
46
  - **区块折叠**:明细、模型、Token 分布各自独立折叠
47
47
  - **折叠记忆**:折叠状态持久化,重启后保持
48
48
  - **语言适配**:自动检测系统语言
49
+ - **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
50
+ - **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` 动态配置面板
49
51
 
50
52
  ---
51
53
 
52
- ## 安装
54
+ ## 3. 安装
53
55
 
54
- ### 方式一:OpenCode 命令安装(推荐)
56
+ ### 3.1 方式一:OpenCode 命令安装(推荐)
55
57
 
56
58
  在 OpenCode 中按 **`Ctrl + P`** 打开命令面板,搜索 **`install plugin`**,输入:
57
59
 
@@ -61,7 +63,7 @@ opencode-visual-cache@latest
61
63
 
62
64
  回车即可完成安装与配置。
63
65
 
64
- ### 方式二:手动安装
66
+ ### 3.2 方式二:手动安装
65
67
 
66
68
  **1. 安装插件**
67
69
 
@@ -80,13 +82,62 @@ npm install -g opencode-visual-cache@latest
80
82
  }
81
83
  ```
82
84
 
83
- ### 重启 OpenCode
85
+ ### 3.3 重启 OpenCode
84
86
 
85
87
  进入任意 session,侧边栏即可看到缓存统计面板。
86
88
 
87
89
  ---
88
90
 
89
- ## 更新
91
+ ## 4. 使用指南
92
+
93
+ ### 4.1 斜杠命令
94
+
95
+ 插件支持通过斜杠命令或命令面板(`Ctrl + P`)动态调整配置,所有设置即时生效并持久化:
96
+
97
+ | 命令 | 功能 | 使用方式 |
98
+ |------|------|---------|
99
+ | `/cache-currency` | 切换货币单位 | 从列表选择货币(USD / CNY / EUR / JPY / GBP / KRW),自动填入默认汇率 |
100
+ | `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 |
101
+ | `/cache-section` | 开关区块显示 | 独立控制「Token 明细」「模型与定价」「估算 Token 分布」的显隐 |
102
+ | `/cache-config` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 |
103
+
104
+ <div align="center">
105
+ <img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="斜杠命令" width="49%"></img>
106
+ <img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/ctrlP_cmd.png" alt="Ctrl+P 命令面板" width="49%"></img>
107
+ </div>
108
+
109
+ 切换货币时会自动填入离线内置的近似汇率(以 USD 为基准),用户可随时通过 `/cache-rate` 自定义。
110
+
111
+ ### 4.2 货币与汇率
112
+
113
+ 费用展示支持多币种切换:
114
+
115
+ | 货币代码 | 符号 | 默认汇率(1 USD = ?) |
116
+ |---------|------|---------------------|
117
+ | USD | `$` | 1 |
118
+ | CNY | `¥` | 7.2 |
119
+ | EUR | `€` | 0.92 |
120
+ | JPY | `JP¥` | 150 |
121
+ | GBP | `£` | 0.79 |
122
+ | KRW | `₩` | 1350 |
123
+
124
+ > 汇率会同步应用到 Session 累计费用、缓存节省金额、以及模型单价展示。
125
+ >
126
+ > **基币说明**:插件假设提供商定价均为美元(USD)。目前主流 AI API(OpenAI / Anthropic / Google / DeepSeek / xAI 等)的国际版均以 USD 计价。如果你使用的提供商以人民币或其他货币计价,请将汇率设为 `1`。
127
+
128
+ ### 4.3 区块可见性
129
+
130
+ 面板中的三个子区块可以独立关闭,方便在侧边栏空间紧张时隐藏不需要的信息:
131
+
132
+ - **Token 明细**:缓存读 / 缓存写 / 未命中 / 输出
133
+ - **模型与定价**:费用 / 提供商 / 模型名 / 单价
134
+ - **估算 Token 分布**:按角色拆分的 Token 估算
135
+
136
+ 通过 `/cache-section` 切换后即时生效,无需重启。
137
+
138
+ ---
139
+
140
+ ## 5. 更新
90
141
 
91
142
  由于 [OpenCode 已知问题 #6774](https://github.com/anomalyco/opencode/issues/6774),插件缓存会锁死在首次安装时的版本,不会自动检测 npm 上的新版本。
92
143
 
@@ -112,7 +163,7 @@ rm -rf ~/.cache/opencode/packages/opencode-visual-cache@latest
112
163
 
113
164
  ---
114
165
 
115
- ## 调试
166
+ ## 6. 调试
116
167
 
117
168
  强制英文(Windows PowerShell):
118
169
 
@@ -122,13 +173,13 @@ $env:CACHE_TUI_LANG="en"; opencode
122
173
 
123
174
  ---
124
175
 
125
- ## 兼容性
176
+ ## 7. 兼容性
126
177
 
127
178
  代码完全模型无关,支持所有 OpenCode 兼容的 AI 模型(DeepSeek / Claude / GPT 等)。
128
179
  Token 数据和定价信息均通过 OpenCode SDK 标准接口获取。
129
180
 
130
181
  ---
131
182
 
132
- ## License
183
+ ## 8. License
133
184
 
134
185
  MIT
package/README_EN.md CHANGED
@@ -19,7 +19,7 @@ If you find this plugin useful, a ⭐ would mean a lot — thank you!<br>
19
19
 
20
20
  ---
21
21
 
22
- ## Screenshots
22
+ ## 1. Screenshots
23
23
 
24
24
  <div align="center">
25
25
  <strong>Collapsed 👇</strong> <br>
@@ -34,24 +34,25 @@ If you find this plugin useful, a ⭐ would mean a lot — thank you!<br>
34
34
 
35
35
  ---
36
36
 
37
- ## Features
38
-
39
- - **Cache Hit Rate**: Real-time `cache_read / total_input × 100%` with adaptive-width progress bar
40
- - **Token Breakdown**: Cache Read / Write / Miss / Output, left-aligned labels, right-aligned values
41
- - **Cost & Savings**: Session cost + cache savings (input_rate − cache_read_rate) × cache_read
42
- - **Model Pricing**: Current model's input / cache-read rates (from provider config)
43
- - **Collapsible**: Click title to fold into one line
44
- - **Adaptive Colors**: ≥85% green · ≥70% orange · <70% red, auto-desaturated from theme
45
- - **Token Distribution**: Per-role (system / user / agent instr / tool call / tool result) parseable token breakdown
46
- - **Collapsible Sections**: Detail, model, and token distribution fold independently
47
- - **Persistent Fold State**: Fold preferences remembered across restarts
37
+ ## 2. Features
38
+
39
+ - **Cache Hit Rate**: Real-time hit rate with adaptive-width progress bar and trend indicator
40
+ - **Token Detail**: Cache read / write / miss / output, left-aligned labels, right-aligned values
41
+ - **Cost & Savings**: Session cumulative cost plus cache-hit savings
42
+ - **Model Pricing**: Input / cache-read / cache-write per-million rates (read from provider config dynamically)
43
+ - **Collapsible**: Main title collapsed by default; click to expand. Detail, model, and distribution sections fold independently
44
+ - **Adaptive Colors**: ≥85% green · ≥70% orange · <70% red, auto-desaturated from current theme
45
+ - **Token Distribution**: Per-role (system / user / agent instr / tool call / tool result) estimated token breakdown
46
+ - **Persistent State**: Fold preferences and config remembered across restarts via api.kv
48
47
  - **Language**: Auto-detects system locale
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
50
 
50
51
  ---
51
52
 
52
- ## Installation
53
+ ## 3. Installation
53
54
 
54
- ### Option 1: OpenCode Command (recommended)
55
+ ### 3.1 Option 1: OpenCode Command (recommended)
55
56
 
56
57
  Press **`Ctrl + P`** in OpenCode to open the command palette, search **`install plugin`**, then type:
57
58
 
@@ -61,7 +62,7 @@ opencode-visual-cache@latest
61
62
 
62
63
  Press Enter to install and configure automatically.
63
64
 
64
- ### Option 2: Manual
65
+ ### 3.2 Option 2: Manual
65
66
 
66
67
  **1. Install the plugin**
67
68
 
@@ -80,13 +81,62 @@ Create or edit `~/.config/opencode/tui.jsonc`:
80
81
  }
81
82
  ```
82
83
 
83
- ### Restart OpenCode
84
+ ### 3.3 Restart OpenCode
84
85
 
85
86
  Open any session — the cache stats panel appears in the sidebar.
86
87
 
87
88
  ---
88
89
 
89
- ## Update
90
+ ## 4. Usage Guide
91
+
92
+ ### 4.1 Slash Commands
93
+
94
+ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime configuration. All changes take effect immediately and are persisted:
95
+
96
+ | Command | Function | How to use |
97
+ |---------|----------|------------|
98
+ | `/cache-currency` | Switch currency | Pick from a list (USD / CNY / EUR / JPY / GBP / KRW); default exchange rate auto-filled |
99
+ | `/cache-rate` | Adjust exchange rate | Enter a custom rate (e.g. `7.2` for CNY) |
100
+ | `/cache-section` | Toggle sections | Independently show/hide Detail, Model & Pricing, or Token Distribution |
101
+ | `/cache-config` | View current config | Displays currency, rate, and section visibility |
102
+
103
+ <div align="center">
104
+ <img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="Slash command" width="49%"></img>
105
+ <img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/ctrlP_cmd.png" alt="Ctrl+P command palette" width="49%"></img>
106
+ </div>
107
+
108
+ Switching currency automatically applies a built-in approximate exchange rate (USD-based). Override it anytime with `/cache-rate`.
109
+
110
+ ### 4.2 Currency & Exchange Rate
111
+
112
+ Cost display supports multiple currencies:
113
+
114
+ | Code | Symbol | Default rate (1 USD = ?) |
115
+ |------|--------|-------------------------|
116
+ | USD | `$` | 1 |
117
+ | CNY | `¥` | 7.2 |
118
+ | EUR | `€` | 0.92 |
119
+ | JPY | `JP¥` | 150 |
120
+ | GBP | `£` | 0.79 |
121
+ | KRW | `₩` | 1350 |
122
+
123
+ > The rate applies to session cost, cache savings, and per-million pricing — consistently across the panel.
124
+ >
125
+ > **Base currency**: The plugin assumes all provider pricing is in USD. Major AI APIs (OpenAI / Anthropic / Google / DeepSeek / xAI etc.) use USD for their international endpoints. If your provider bills in CNY or another currency, set the exchange rate to `1`.
126
+
127
+ ### 4.3 Section Visibility
128
+
129
+ Three sub-sections can be toggled independently to save sidebar space:
130
+
131
+ - **Token Detail**: cache read / write / miss / output
132
+ - **Model & Pricing**: cost / provider / model name / per-million rates
133
+ - **Estimated Token Dist.**: per-role token breakdown
134
+
135
+ Toggled via `/cache-section` — takes effect instantly, no restart required.
136
+
137
+ ---
138
+
139
+ ## 5. Update
90
140
 
91
141
  Due to a [known OpenCode issue #6774](https://github.com/anomalyco/opencode/issues/6774), the plugin cache locks to the version installed at first setup and does **not** auto-detect newer releases on npm.
92
142
 
@@ -112,7 +162,7 @@ Press **`Ctrl + P`** in OpenCode → `install plugin` → `opencode-visual-cache
112
162
 
113
163
  ---
114
164
 
115
- ## Debug
165
+ ## 6. Debug
116
166
 
117
167
  Force English:
118
168
 
@@ -128,13 +178,13 @@ CACHE_TUI_LANG=en opencode
128
178
 
129
179
  ---
130
180
 
131
- ## Compatibility
181
+ ## 7. Compatibility
132
182
 
133
183
  Model-agnostic — works with all OpenCode-compatible AI models (DeepSeek / Claude / GPT etc.).
134
184
  Token data and pricing are read via OpenCode SDK standard interfaces.
135
185
 
136
186
  ---
137
187
 
138
- ## License
188
+ ## 8. License
139
189
 
140
190
  MIT
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.4";
1
+ export declare const PLUGIN_VERSION = "1.2.6";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.2.4";
2
+ export const PLUGIN_VERSION = "1.2.6";
package/dist/index.js CHANGED
@@ -222,12 +222,13 @@ function fmt(n) {
222
222
  function num(v) {
223
223
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
224
224
  }
225
- function fmtCost(n) {
226
- if (n >= 1)
227
- return "$" + n.toFixed(2);
228
- if (n >= 0.01)
229
- return "$" + n.toFixed(3);
230
- return "$" + n.toFixed(4);
225
+ function fmtCost(n, symbol = "$", rate = 1) {
226
+ const v = n * rate;
227
+ if (v >= 1)
228
+ return symbol + v.toFixed(2);
229
+ if (v >= 0.01)
230
+ return symbol + v.toFixed(3);
231
+ return symbol + v.toFixed(4);
231
232
  }
232
233
  // ── token estimation ──
233
234
  // Character-based BPE approximation. Default ratios (~4 ASCII or ~1.5 CJK
@@ -269,6 +270,21 @@ function estimateTokens(text) {
269
270
  // ---------------------------------------------------------------------------
270
271
  // Sidebar component
271
272
  // ---------------------------------------------------------------------------
273
+ /** Module-level config signals — shared between the TUI component and slash
274
+ * commands so that runtime changes take effect immediately without restart. */
275
+ const [currencySymbol, setCurrencySymbol] = createSignal("$");
276
+ const [exchangeRate, setExchangeRate] = createSignal(1);
277
+ const [sectionDetail, setSectionDetail] = createSignal(true);
278
+ const [sectionModel, setSectionModel] = createSignal(true);
279
+ const [sectionDist, setSectionDist] = createSignal(true);
280
+ const CURRENCIES = {
281
+ USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
282
+ };
283
+ /** Approximate USD exchange rates — used as defaults when switching currency.
284
+ * Users can override via /cache-rate. Last updated 2026-05. */
285
+ const DEFAULT_RATES = {
286
+ USD: 1, CNY: 7.2, EUR: 0.92, JPY: 150, GBP: 0.79, KRW: 1350,
287
+ };
272
288
  const MIN_PANEL_WIDTH = 20;
273
289
  const DEFAULT_PANEL_WIDTH = 26;
274
290
  /** Horizontal space eaten by border (1+1) + padding (2+2). */
@@ -500,6 +516,19 @@ function TokenCachePanel(props) {
500
516
  setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
501
517
  }
502
518
  catch { }
519
+ // Restore user config (currency, rate, section visibility)
520
+ try {
521
+ const sym = props.api.kv.get(`${KV_PREFIX}.currency`);
522
+ const rate = props.api.kv.get(`${KV_PREFIX}.rate`);
523
+ if (typeof sym === "string")
524
+ setCurrencySymbol(sym);
525
+ if (typeof rate === "number" && rate > 0)
526
+ setExchangeRate(rate);
527
+ setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
528
+ setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
529
+ setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
530
+ }
531
+ catch { }
503
532
  const unsubPart = props.api.event.on("message.part.updated", () => {
504
533
  setPartVersion((v) => v + 1);
505
534
  });
@@ -554,7 +583,7 @@ function TokenCachePanel(props) {
554
583
  // boxEl.width may be undefined before the first measurement — guard with 0
555
584
  const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
556
585
  setPanelWidth((prev) => (prev === w ? prev : w));
557
- }, 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("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)))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved)] })] }) })] }), _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)) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, "$" + data().inputRate.toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + data().cacheReadRate.toFixed(2) + "/M " + T.cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", "$" + data().cacheWriteRate.toFixed(2) + "/M " + T.writeRate) }) })] })] }), _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.system + data().dist.user + data().dist.agent + data().dist.toolCall + data().dist.toolResult), T.tok) })] })] })] }) })] }));
586
+ }, 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.system + data().dist.user + data().dist.agent + data().dist.toolCall + data().dist.toolResult), T.tok) })] })] }) })] }) })] }));
558
587
  }
559
588
  // ---------------------------------------------------------------------------
560
589
  // Plugin entry
@@ -571,6 +600,95 @@ function createSidebarSlot(api) {
571
600
  }
572
601
  const tui = async (api) => {
573
602
  api.slots.register(createSidebarSlot(api));
603
+ // ── slash commands for runtime config ──
604
+ const KV_PREFIX = "cache_panel";
605
+ api.command?.register(() => [
606
+ {
607
+ title: "Cache: Set Currency",
608
+ value: "cache.currency",
609
+ description: "Change the currency unit for cost display",
610
+ slash: { name: "cache-currency" },
611
+ onSelect: (dialog) => {
612
+ dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Select Currency", options: Object.entries(CURRENCIES).map(([code, sym]) => ({
613
+ title: `${code} (${sym})`,
614
+ value: code,
615
+ })), onSelect: (opt) => {
616
+ const sym = CURRENCIES[opt.value] ?? "$";
617
+ const defRate = DEFAULT_RATES[opt.value] ?? 1;
618
+ api.kv.set(`${KV_PREFIX}.currency`, sym);
619
+ api.kv.set(`${KV_PREFIX}.rate`, defRate);
620
+ setCurrencySymbol(sym);
621
+ setExchangeRate(defRate);
622
+ api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
623
+ dialog?.clear();
624
+ } })));
625
+ },
626
+ },
627
+ {
628
+ title: "Cache: Set Exchange Rate",
629
+ value: "cache.rate",
630
+ description: "Set the exchange rate multiplier for the selected currency",
631
+ slash: { name: "cache-rate" },
632
+ onSelect: (dialog) => {
633
+ dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: "Exchange Rate", description: () => _jsx("text", { children: "Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)" }), placeholder: "1.0", value: String(api.kv.get(`${KV_PREFIX}.rate`, 1)), onConfirm: (val) => {
634
+ const n = parseFloat(val);
635
+ if (n > 0) {
636
+ api.kv.set(`${KV_PREFIX}.rate`, n);
637
+ setExchangeRate(n);
638
+ api.ui.toast({ message: `Exchange rate set to ${n}` });
639
+ }
640
+ dialog?.clear();
641
+ } })));
642
+ },
643
+ },
644
+ {
645
+ title: "Cache: Toggle Section",
646
+ value: "cache.section",
647
+ description: "Show or hide a sidebar section",
648
+ slash: { name: "cache-section" },
649
+ onSelect: (dialog) => {
650
+ const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
651
+ const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
652
+ const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
653
+ dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
654
+ { title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
655
+ { title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
656
+ { title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
657
+ ], onSelect: (opt) => {
658
+ const key = `${KV_PREFIX}.section.${opt.value}`;
659
+ const cur = Boolean(api.kv.get(key, true));
660
+ api.kv.set(key, !cur);
661
+ if (opt.value === "detail")
662
+ setSectionDetail(!cur);
663
+ if (opt.value === "model")
664
+ setSectionModel(!cur);
665
+ if (opt.value === "dist")
666
+ setSectionDist(!cur);
667
+ api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
668
+ dialog?.clear();
669
+ } })));
670
+ },
671
+ },
672
+ {
673
+ title: "Cache: Show Config",
674
+ value: "cache.config",
675
+ description: "Display the current plugin configuration",
676
+ slash: { name: "cache-config" },
677
+ onSelect: (dialog) => {
678
+ const sym = api.kv.get(`${KV_PREFIX}.currency`) ?? "$";
679
+ const rate = api.kv.get(`${KV_PREFIX}.rate`) ?? 1;
680
+ const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
681
+ const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
682
+ const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
683
+ api.ui.toast({
684
+ title: "Cache Panel Config",
685
+ message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
686
+ duration: 8000,
687
+ });
688
+ dialog?.clear();
689
+ },
690
+ },
691
+ ]);
574
692
  };
575
693
  const mod = {
576
694
  id: "opencode-visual-cache",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
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.4";
2
+ export const PLUGIN_VERSION="1.2.6";
package/src/index.tsx CHANGED
@@ -249,10 +249,11 @@ function num(v: unknown): number {
249
249
  return typeof v === "number" && Number.isFinite(v) ? v : 0
250
250
  }
251
251
 
252
- function fmtCost(n: number): string {
253
- if (n >= 1) return "$" + n.toFixed(2)
254
- if (n >= 0.01) return "$" + n.toFixed(3)
255
- return "$" + n.toFixed(4)
252
+ function fmtCost(n: number, symbol = "$", rate = 1): string {
253
+ const v = n * rate
254
+ if (v >= 1) return symbol + v.toFixed(2)
255
+ if (v >= 0.01) return symbol + v.toFixed(3)
256
+ return symbol + v.toFixed(4)
256
257
  }
257
258
 
258
259
  // ── token estimation ──
@@ -305,6 +306,23 @@ interface TokenDist {
305
306
  // Sidebar component
306
307
  // ---------------------------------------------------------------------------
307
308
 
309
+ /** Module-level config signals — shared between the TUI component and slash
310
+ * commands so that runtime changes take effect immediately without restart. */
311
+ const [currencySymbol, setCurrencySymbol] = createSignal("$")
312
+ const [exchangeRate, setExchangeRate] = createSignal(1)
313
+ const [sectionDetail, setSectionDetail] = createSignal(true)
314
+ const [sectionModel, setSectionModel] = createSignal(true)
315
+ const [sectionDist, setSectionDist] = createSignal(true)
316
+
317
+ const CURRENCIES: Record<string, string> = {
318
+ USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
319
+ }
320
+ /** Approximate USD exchange rates — used as defaults when switching currency.
321
+ * Users can override via /cache-rate. Last updated 2026-05. */
322
+ const DEFAULT_RATES: Record<string, number> = {
323
+ USD: 1, CNY: 7.2, EUR: 0.92, JPY: 150, GBP: 0.79, KRW: 1350,
324
+ }
325
+
308
326
  const MIN_PANEL_WIDTH = 20
309
327
  const DEFAULT_PANEL_WIDTH = 26
310
328
  /** Horizontal space eaten by border (1+1) + padding (2+2). */
@@ -529,6 +547,17 @@ function TokenCachePanel(props: {
529
547
  setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
530
548
  } catch {}
531
549
 
550
+ // Restore user config (currency, rate, section visibility)
551
+ try {
552
+ const sym = props.api.kv.get<string>(`${KV_PREFIX}.currency`)
553
+ const rate = props.api.kv.get<number>(`${KV_PREFIX}.rate`)
554
+ if (typeof sym === "string") setCurrencySymbol(sym)
555
+ if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
556
+ setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
557
+ setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
558
+ setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
559
+ } catch {}
560
+
532
561
  const unsubPart = props.api.event.on("message.part.updated", () => {
533
562
  setPartVersion((v) => v + 1)
534
563
  })
@@ -658,6 +687,7 @@ function TokenCachePanel(props: {
658
687
  </text>
659
688
 
660
689
  {/* ── detail section (collapsible, default open) ── */}
690
+ <Show when={sectionDetail()}>
661
691
  <text onMouseUp={() => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n })}>
662
692
  <span style={{ fg: pal().muted }}>{detailOpen() ? "\u25bc " : "\u25b6 "}</span>
663
693
  <span style={{ fg: pal().primary }}><b>{T.secDetail}</b></span>
@@ -684,13 +714,15 @@ function TokenCachePanel(props: {
684
714
  <Show when={data().saved > 0}>
685
715
  <text>
686
716
  <span style={{ fg: pal().muted }}>{T.saved}</span>
687
- <span>{" ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved))))}</span>
688
- <span style={{ fg: pal().success }}>~{fmtCost(data().saved)}</span>
717
+ <span>{" ".repeat(Math.max(1, panelWidth() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
718
+ <span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
689
719
  </text>
690
720
  </Show>
691
721
  </Show>
722
+ </Show>
692
723
 
693
724
  {/* ── model section (collapsible, default open) ── */}
725
+ <Show when={sectionModel()}>
694
726
  {<text onMouseUp={() => setModelOpen((o) => { const n = !o; persistFold("model", n); return n })}>
695
727
  <span style={{ fg: pal().muted }}>{modelOpen() ? "\u25bc " : "\u25b6 "}</span>
696
728
  <span style={{ fg: pal().primary }}><b>{T.secModel}</b></span>
@@ -699,7 +731,7 @@ function TokenCachePanel(props: {
699
731
 
700
732
  <Show when={modelOpen()}>
701
733
  <text fg={pal().text}>
702
- {justify(T.cost, fmtCost(data().cost))}
734
+ {justify(T.cost, fmtCost(data().cost, currencySymbol(), exchangeRate()))}
703
735
  </text>
704
736
  <Show when={data().providerName}>
705
737
  <text fg={pal().muted}>
@@ -711,22 +743,24 @@ function TokenCachePanel(props: {
711
743
  </text>
712
744
  <Show when={data().hasPricing}>
713
745
  <text fg={pal().muted}>
714
- {justify(T.rate, "$" + data().inputRate.toFixed(2) + "/M " + T.inputRate)}
746
+ {justify(T.rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + T.inputRate)}
715
747
  </text>
716
748
  <Show when={data().cacheReadRate > 0}>
717
749
  <text fg={pal().muted}>
718
- {justify("", "$" + data().cacheReadRate.toFixed(2) + "/M " + T.cacheRate)}
750
+ {justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + T.cacheRate)}
719
751
  </text>
720
752
  </Show>
721
753
  <Show when={data().cacheWriteRate > 0}>
722
754
  <text fg={pal().muted}>
723
- {justify("", "$" + data().cacheWriteRate.toFixed(2) + "/M " + T.writeRate)}
755
+ {justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + T.writeRate)}
724
756
  </text>
725
- </Show>
726
757
  </Show>
727
758
  </Show>
759
+ </Show>
760
+ </Show>
728
761
 
729
762
  {/* ── token distribution (collapsible, default closed) ── */}
763
+ <Show when={sectionDist()}>
730
764
  <Show when={data().hasDistData}>
731
765
  {<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
732
766
  <span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
@@ -764,6 +798,7 @@ function TokenCachePanel(props: {
764
798
  </text>
765
799
  </Show>
766
800
  </Show>
801
+ </Show>
767
802
  </Show>
768
803
  </Show>
769
804
  </box>
@@ -793,6 +828,113 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
793
828
 
794
829
  const tui: TuiPlugin = async (api: TuiPluginApi) => {
795
830
  api.slots.register(createSidebarSlot(api))
831
+
832
+ // ── slash commands for runtime config ──
833
+ const KV_PREFIX = "cache_panel"
834
+ api.command?.register(() => [
835
+ {
836
+ title: "Cache: Set Currency",
837
+ value: "cache.currency",
838
+ description: "Change the currency unit for cost display",
839
+ slash: { name: "cache-currency" },
840
+ onSelect: (dialog) => {
841
+ dialog?.replace(() => (
842
+ <api.ui.DialogSelect
843
+ title="Select Currency"
844
+ options={Object.entries(CURRENCIES).map(([code, sym]) => ({
845
+ title: `${code} (${sym})`,
846
+ value: code,
847
+ }))}
848
+ onSelect={(opt) => {
849
+ const sym = CURRENCIES[opt.value] ?? "$"
850
+ const defRate = DEFAULT_RATES[opt.value] ?? 1
851
+ api.kv.set(`${KV_PREFIX}.currency`, sym)
852
+ api.kv.set(`${KV_PREFIX}.rate`, defRate)
853
+ setCurrencySymbol(sym)
854
+ setExchangeRate(defRate)
855
+ api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` })
856
+ dialog?.clear()
857
+ }}
858
+ />
859
+ ))
860
+ },
861
+ },
862
+ {
863
+ title: "Cache: Set Exchange Rate",
864
+ value: "cache.rate",
865
+ description: "Set the exchange rate multiplier for the selected currency",
866
+ slash: { name: "cache-rate" },
867
+ onSelect: (dialog) => {
868
+ dialog?.replace(() => (
869
+ <api.ui.DialogPrompt
870
+ title="Exchange Rate"
871
+ description={() => <text>Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)</text>}
872
+ placeholder="1.0"
873
+ value={String(api.kv.get<number>(`${KV_PREFIX}.rate`, 1))}
874
+ onConfirm={(val) => {
875
+ const n = parseFloat(val)
876
+ if (n > 0) {
877
+ api.kv.set(`${KV_PREFIX}.rate`, n)
878
+ setExchangeRate(n)
879
+ api.ui.toast({ message: `Exchange rate set to ${n}` })
880
+ }
881
+ dialog?.clear()
882
+ }}
883
+ />
884
+ ))
885
+ },
886
+ },
887
+ {
888
+ title: "Cache: Toggle Section",
889
+ value: "cache.section",
890
+ description: "Show or hide a sidebar section",
891
+ slash: { name: "cache-section" },
892
+ onSelect: (dialog) => {
893
+ const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
894
+ const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
895
+ const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
896
+ dialog?.replace(() => (
897
+ <api.ui.DialogSelect
898
+ title="Toggle Section"
899
+ options={[
900
+ { title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
901
+ { title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
902
+ { title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
903
+ ]}
904
+ onSelect={(opt) => {
905
+ const key = `${KV_PREFIX}.section.${opt.value}`
906
+ const cur = Boolean(api.kv.get(key, true))
907
+ api.kv.set(key, !cur)
908
+ if (opt.value === "detail") setSectionDetail(!cur)
909
+ if (opt.value === "model") setSectionModel(!cur)
910
+ if (opt.value === "dist") setSectionDist(!cur)
911
+ api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
912
+ dialog?.clear()
913
+ }}
914
+ />
915
+ ))
916
+ },
917
+ },
918
+ {
919
+ title: "Cache: Show Config",
920
+ value: "cache.config",
921
+ description: "Display the current plugin configuration",
922
+ slash: { name: "cache-config" },
923
+ onSelect: (dialog) => {
924
+ const sym = api.kv.get<string>(`${KV_PREFIX}.currency`) ?? "$"
925
+ const rate = api.kv.get<number>(`${KV_PREFIX}.rate`) ?? 1
926
+ const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
927
+ const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
928
+ const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
929
+ api.ui.toast({
930
+ title: "Cache Panel Config",
931
+ message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
932
+ duration: 8000,
933
+ })
934
+ dialog?.clear()
935
+ },
936
+ },
937
+ ])
796
938
  }
797
939
 
798
940
  const mod: TuiPluginModule & { id: string } = {