opencode-visual-cache 1.2.6 → 1.2.7
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 +9 -4
- package/README_EN.md +2 -2
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +31 -17
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +33 -18
package/README.md
CHANGED
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
- **折叠面板**:主标题默认折叠,点击展开;明细、模型、分布各自独立折叠
|
|
44
44
|
- **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
|
|
45
45
|
- **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool 结果)展示估算 Token 占比
|
|
46
|
-
- **区块折叠**:明细、模型、Token 分布各自独立折叠
|
|
47
46
|
- **折叠记忆**:折叠状态持久化,重启后保持
|
|
48
47
|
- **语言适配**:自动检测系统语言
|
|
49
48
|
- **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
|
|
@@ -98,7 +97,7 @@ npm install -g opencode-visual-cache@latest
|
|
|
98
97
|
|------|------|---------|
|
|
99
98
|
| `/cache-currency` | 切换货币单位 | 从列表选择货币(USD / CNY / EUR / JPY / GBP / KRW),自动填入默认汇率 |
|
|
100
99
|
| `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 |
|
|
101
|
-
| `/cache-section` |
|
|
100
|
+
| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 面板边框的显隐 |
|
|
102
101
|
| `/cache-config` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 |
|
|
103
102
|
|
|
104
103
|
<div align="center">
|
|
@@ -133,7 +132,7 @@ npm install -g opencode-visual-cache@latest
|
|
|
133
132
|
- **模型与定价**:费用 / 提供商 / 模型名 / 单价
|
|
134
133
|
- **估算 Token 分布**:按角色拆分的 Token 估算
|
|
135
134
|
|
|
136
|
-
通过 `/cache-section`
|
|
135
|
+
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
137
136
|
|
|
138
137
|
---
|
|
139
138
|
|
|
@@ -165,12 +164,18 @@ rm -rf ~/.cache/opencode/packages/opencode-visual-cache@latest
|
|
|
165
164
|
|
|
166
165
|
## 6. 调试
|
|
167
166
|
|
|
168
|
-
|
|
167
|
+
强制英文:
|
|
169
168
|
|
|
170
169
|
```powershell
|
|
170
|
+
# Windows PowerShell
|
|
171
171
|
$env:CACHE_TUI_LANG="en"; opencode
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
```bash
|
|
175
|
+
# macOS / Linux
|
|
176
|
+
CACHE_TUI_LANG=en opencode
|
|
177
|
+
```
|
|
178
|
+
|
|
174
179
|
---
|
|
175
180
|
|
|
176
181
|
## 7. 兼容性
|
package/README_EN.md
CHANGED
|
@@ -97,7 +97,7 @@ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime
|
|
|
97
97
|
|---------|----------|------------|
|
|
98
98
|
| `/cache-currency` | Switch currency | Pick from a list (USD / CNY / EUR / JPY / GBP / KRW); default exchange rate auto-filled |
|
|
99
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,
|
|
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
102
|
|
|
103
103
|
<div align="center">
|
|
@@ -132,7 +132,7 @@ Three sub-sections can be toggled independently to save sidebar space:
|
|
|
132
132
|
- **Model & Pricing**: cost / provider / model name / per-million rates
|
|
133
133
|
- **Estimated Token Dist.**: per-role token breakdown
|
|
134
134
|
|
|
135
|
-
Toggled via `/cache-section` — takes effect instantly, no restart required.
|
|
135
|
+
Toggled via `/cache-section` — takes effect instantly, no restart required. The same command also toggles the panel **border**; turning it off removes the outline and padding so content fills the full width.
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.2.7";
|
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.7";
|
package/dist/index.js
CHANGED
|
@@ -277,6 +277,7 @@ const [exchangeRate, setExchangeRate] = createSignal(1);
|
|
|
277
277
|
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
278
278
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
279
279
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
280
|
+
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
280
281
|
const CURRENCIES = {
|
|
281
282
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
282
283
|
};
|
|
@@ -287,8 +288,6 @@ const DEFAULT_RATES = {
|
|
|
287
288
|
};
|
|
288
289
|
const MIN_PANEL_WIDTH = 20;
|
|
289
290
|
const DEFAULT_PANEL_WIDTH = 26;
|
|
290
|
-
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
291
|
-
const GUTTER = 2 + 4;
|
|
292
291
|
/** ── layout measurement constants (visual columns) ── */
|
|
293
292
|
const LABEL_GAP = 1; // label(如 "Hit")后面的空格
|
|
294
293
|
const BAR_BRACKETS = 2; // "[" + "]" 包围进度条
|
|
@@ -527,8 +526,11 @@ function TokenCachePanel(props) {
|
|
|
527
526
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
528
527
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
529
528
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
529
|
+
const borderVal = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
530
|
+
setBorderVisible(borderVal !== false);
|
|
530
531
|
}
|
|
531
532
|
catch { }
|
|
533
|
+
// (border visibility was restored above together with section config)
|
|
532
534
|
const unsubPart = props.api.event.on("message.part.updated", () => {
|
|
533
535
|
setPartVersion((v) => v + 1);
|
|
534
536
|
});
|
|
@@ -561,29 +563,31 @@ function TokenCachePanel(props) {
|
|
|
561
563
|
return pal().warning;
|
|
562
564
|
return pal().error;
|
|
563
565
|
});
|
|
564
|
-
|
|
566
|
+
/** Horizontal space eaten by border (1+1 when visible) + padding (2+2 when visible). */
|
|
567
|
+
const gutter = createMemo(() => borderVisible() ? 6 : 0);
|
|
568
|
+
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())));
|
|
565
569
|
function trendLabel(t) {
|
|
566
570
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "");
|
|
567
571
|
}
|
|
568
572
|
const barW = createMemo(() => {
|
|
569
573
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
570
|
-
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace +
|
|
574
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
571
575
|
return Math.max(3, panelWidth() - overhead);
|
|
572
576
|
});
|
|
573
577
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
574
578
|
const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%");
|
|
575
579
|
// left-align label, right-align value — auto-fill space between
|
|
576
580
|
const justify = (label, value, unit = "") => {
|
|
577
|
-
const gauge = panelWidth() -
|
|
581
|
+
const gauge = panelWidth() - gutter();
|
|
578
582
|
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0);
|
|
579
583
|
const gap = Math.max(1, gauge - used);
|
|
580
584
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
581
585
|
};
|
|
582
|
-
return (_jsxs("box", { border:
|
|
586
|
+
return (_jsxs("box", { border: borderVisible(), borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: borderVisible() ? 2 : 0, paddingRight: borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
583
587
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
584
588
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
585
589
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
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() -
|
|
590
|
+
}, 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) })] })] }) })] }) })] }));
|
|
587
591
|
}
|
|
588
592
|
// ---------------------------------------------------------------------------
|
|
589
593
|
// Plugin entry
|
|
@@ -650,21 +654,31 @@ const tui = async (api) => {
|
|
|
650
654
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
651
655
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
652
656
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
657
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
653
658
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
|
|
654
659
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
655
660
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
656
661
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
662
|
+
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
657
663
|
], onSelect: (opt) => {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
664
|
+
if (opt.value === "border") {
|
|
665
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
666
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur);
|
|
667
|
+
setBorderVisible(!cur);
|
|
668
|
+
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` });
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
const key = `${KV_PREFIX}.section.${opt.value}`;
|
|
672
|
+
const cur = Boolean(api.kv.get(key, true));
|
|
673
|
+
api.kv.set(key, !cur);
|
|
674
|
+
if (opt.value === "detail")
|
|
675
|
+
setSectionDetail(!cur);
|
|
676
|
+
if (opt.value === "model")
|
|
677
|
+
setSectionModel(!cur);
|
|
678
|
+
if (opt.value === "dist")
|
|
679
|
+
setSectionDist(!cur);
|
|
680
|
+
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
681
|
+
}
|
|
668
682
|
dialog?.clear();
|
|
669
683
|
} })));
|
|
670
684
|
},
|
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.7";
|
package/src/index.tsx
CHANGED
|
@@ -313,6 +313,7 @@ const [exchangeRate, setExchangeRate] = createSignal(1)
|
|
|
313
313
|
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
314
314
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
315
315
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
316
|
+
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
316
317
|
|
|
317
318
|
const CURRENCIES: Record<string, string> = {
|
|
318
319
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
@@ -325,8 +326,6 @@ const DEFAULT_RATES: Record<string, number> = {
|
|
|
325
326
|
|
|
326
327
|
const MIN_PANEL_WIDTH = 20
|
|
327
328
|
const DEFAULT_PANEL_WIDTH = 26
|
|
328
|
-
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
329
|
-
const GUTTER = 2 + 4
|
|
330
329
|
|
|
331
330
|
/** ── layout measurement constants (visual columns) ── */
|
|
332
331
|
const LABEL_GAP = 1 // label(如 "Hit")后面的空格
|
|
@@ -556,8 +555,12 @@ function TokenCachePanel(props: {
|
|
|
556
555
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
557
556
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
558
557
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
558
|
+
const borderVal = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
559
|
+
setBorderVisible(borderVal !== false)
|
|
559
560
|
} catch {}
|
|
560
561
|
|
|
562
|
+
// (border visibility was restored above together with section config)
|
|
563
|
+
|
|
561
564
|
const unsubPart = props.api.event.on("message.part.updated", () => {
|
|
562
565
|
setPartVersion((v) => v + 1)
|
|
563
566
|
})
|
|
@@ -591,14 +594,17 @@ function TokenCachePanel(props: {
|
|
|
591
594
|
return pal().error
|
|
592
595
|
})
|
|
593
596
|
|
|
594
|
-
|
|
597
|
+
/** Horizontal space eaten by border (1+1 when visible) + padding (2+2 when visible). */
|
|
598
|
+
const gutter = createMemo(() => borderVisible() ? 6 : 0)
|
|
599
|
+
|
|
600
|
+
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())))
|
|
595
601
|
function trendLabel(t: number): string {
|
|
596
602
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "")
|
|
597
603
|
}
|
|
598
604
|
|
|
599
605
|
const barW = createMemo(() => {
|
|
600
606
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
601
|
-
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace +
|
|
607
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
602
608
|
return Math.max(3, panelWidth() - overhead)
|
|
603
609
|
})
|
|
604
610
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
@@ -606,7 +612,7 @@ function TokenCachePanel(props: {
|
|
|
606
612
|
|
|
607
613
|
// left-align label, right-align value — auto-fill space between
|
|
608
614
|
const justify = (label: string, value: string, unit = ""): string => {
|
|
609
|
-
const gauge = panelWidth() -
|
|
615
|
+
const gauge = panelWidth() - gutter()
|
|
610
616
|
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
|
|
611
617
|
const gap = Math.max(1, gauge - used)
|
|
612
618
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "")
|
|
@@ -614,12 +620,12 @@ function TokenCachePanel(props: {
|
|
|
614
620
|
|
|
615
621
|
return (
|
|
616
622
|
<box
|
|
617
|
-
border
|
|
623
|
+
border={borderVisible()}
|
|
618
624
|
borderColor={pal().border}
|
|
619
625
|
paddingTop={0}
|
|
620
626
|
paddingBottom={0}
|
|
621
|
-
paddingLeft={2}
|
|
622
|
-
paddingRight={2}
|
|
627
|
+
paddingLeft={borderVisible() ? 2 : 0}
|
|
628
|
+
paddingRight={borderVisible() ? 2 : 0}
|
|
623
629
|
flexDirection="column"
|
|
624
630
|
gap={0}
|
|
625
631
|
ref={boxEl}
|
|
@@ -641,7 +647,7 @@ function TokenCachePanel(props: {
|
|
|
641
647
|
<Show when={!open() && data().hasData}>
|
|
642
648
|
<Show when={data().hasTrendData}>
|
|
643
649
|
<span>
|
|
644
|
-
{" ".repeat(Math.max(1, panelWidth() -
|
|
650
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
|
|
645
651
|
</span>
|
|
646
652
|
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
647
653
|
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
@@ -650,7 +656,7 @@ function TokenCachePanel(props: {
|
|
|
650
656
|
</Show>
|
|
651
657
|
<Show when={!data().hasTrendData}>
|
|
652
658
|
<span>
|
|
653
|
-
{" ".repeat(Math.max(1, panelWidth() -
|
|
659
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
|
|
654
660
|
</span>
|
|
655
661
|
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
656
662
|
</Show>
|
|
@@ -714,7 +720,7 @@ function TokenCachePanel(props: {
|
|
|
714
720
|
<Show when={data().saved > 0}>
|
|
715
721
|
<text>
|
|
716
722
|
<span style={{ fg: pal().muted }}>{T.saved}</span>
|
|
717
|
-
<span>{" ".repeat(Math.max(1, panelWidth() -
|
|
723
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
|
|
718
724
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
719
725
|
</text>
|
|
720
726
|
</Show>
|
|
@@ -893,6 +899,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
893
899
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
894
900
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
895
901
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
902
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
896
903
|
dialog?.replace(() => (
|
|
897
904
|
<api.ui.DialogSelect
|
|
898
905
|
title="Toggle Section"
|
|
@@ -900,15 +907,23 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
900
907
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
901
908
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
902
909
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
910
|
+
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
903
911
|
]}
|
|
904
912
|
onSelect={(opt) => {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
913
|
+
if (opt.value === "border") {
|
|
914
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
915
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
916
|
+
setBorderVisible(!cur)
|
|
917
|
+
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` })
|
|
918
|
+
} else {
|
|
919
|
+
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
920
|
+
const cur = Boolean(api.kv.get(key, true))
|
|
921
|
+
api.kv.set(key, !cur)
|
|
922
|
+
if (opt.value === "detail") setSectionDetail(!cur)
|
|
923
|
+
if (opt.value === "model") setSectionModel(!cur)
|
|
924
|
+
if (opt.value === "dist") setSectionDist(!cur)
|
|
925
|
+
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
926
|
+
}
|
|
912
927
|
dialog?.clear()
|
|
913
928
|
}}
|
|
914
929
|
/>
|