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 +64 -13
- package/README_EN.md +70 -20
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +125 -7
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +153 -11
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
|
-
- **模型定价**:显示当前模型的输入 /
|
|
43
|
-
-
|
|
42
|
+
- **模型定价**:显示当前模型的输入 / 缓存读 / 缓存写单价(从 provider 配置动态读取)
|
|
43
|
+
- **折叠面板**:主标题默认折叠,点击展开;明细、模型、分布各自独立折叠
|
|
44
44
|
- **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
|
|
45
|
-
- **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool
|
|
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
|
|
40
|
-
- **Token
|
|
41
|
-
- **Cost & Savings**: Session cost
|
|
42
|
-
- **Model Pricing**:
|
|
43
|
-
- **Collapsible**:
|
|
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)
|
|
46
|
-
- **
|
|
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
|
-
##
|
|
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
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
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.
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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,
|
|
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
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.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
|
-
|
|
254
|
-
if (
|
|
255
|
-
return
|
|
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,
|
|
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("",
|
|
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("",
|
|
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 } = {
|