opencode-visual-cache 1.2.6 → 1.2.8
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 +139 -48
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +170 -44
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.8";
|
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.8";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
|
|
2
|
-
import { createMemo, createSignal, onMount, onCleanup, Show } from "solid-js";
|
|
2
|
+
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
4
|
// ── terminal-width helpers ────────────────────────────────────────
|
|
5
5
|
// CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
@@ -267,16 +267,6 @@ function estimateTokens(text) {
|
|
|
267
267
|
const asciiPerToken = jsonLike ? 2 : codeLike ? 2.5 : 4;
|
|
268
268
|
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.5));
|
|
269
269
|
}
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Sidebar component
|
|
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
270
|
const CURRENCIES = {
|
|
281
271
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
282
272
|
};
|
|
@@ -287,8 +277,6 @@ const DEFAULT_RATES = {
|
|
|
287
277
|
};
|
|
288
278
|
const MIN_PANEL_WIDTH = 20;
|
|
289
279
|
const DEFAULT_PANEL_WIDTH = 26;
|
|
290
|
-
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
291
|
-
const GUTTER = 2 + 4;
|
|
292
280
|
/** ── layout measurement constants (visual columns) ── */
|
|
293
281
|
const LABEL_GAP = 1; // label(如 "Hit")后面的空格
|
|
294
282
|
const BAR_BRACKETS = 2; // "[" + "]" 包围进度条
|
|
@@ -303,9 +291,21 @@ function TokenCachePanel(props) {
|
|
|
303
291
|
const [modelOpen, setModelOpen] = createSignal(true);
|
|
304
292
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
305
293
|
let boxEl;
|
|
294
|
+
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
295
|
+
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, borderVisible, setBorderVisible, } = props.signals;
|
|
306
296
|
// ── scan session messages reactively ──
|
|
307
297
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
308
298
|
// api.state.session state changes — no event listener needed.
|
|
299
|
+
// ── distribution cache ────────────────────────────────────────
|
|
300
|
+
// When data() re-computes before api.state.part() is warm (e.g. after
|
|
301
|
+
// a view switch), hasDistData flips to false and the distribution
|
|
302
|
+
// block disappears. Keep the last valid snapshot so the UI stays
|
|
303
|
+
// stable until the next successful computation arrives.
|
|
304
|
+
const [lastDist, setLastDist] = createSignal({
|
|
305
|
+
system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
|
|
306
|
+
output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
|
|
307
|
+
});
|
|
308
|
+
const [lastHasDist, setLastHasDist] = createSignal(false);
|
|
309
309
|
const data = createMemo(() => {
|
|
310
310
|
const msgs = props.api.state.session.messages(props.sessionId);
|
|
311
311
|
let input = 0;
|
|
@@ -487,6 +487,10 @@ function TokenCachePanel(props) {
|
|
|
487
487
|
catch {
|
|
488
488
|
// Graceful degradation — dist stays at zeroes
|
|
489
489
|
}
|
|
490
|
+
// Fall back to last known-good distribution while api.state.part()
|
|
491
|
+
// is re-hydrating after a view switch.
|
|
492
|
+
const finalDist = hasDistData ? dist : lastDist();
|
|
493
|
+
const finalHasDist = hasDistData || lastHasDist();
|
|
490
494
|
return {
|
|
491
495
|
hitRate, read, write, freshInput: input, output,
|
|
492
496
|
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
@@ -494,9 +498,24 @@ function TokenCachePanel(props) {
|
|
|
494
498
|
trend, hasTrendData,
|
|
495
499
|
providerName,
|
|
496
500
|
sessionHitRate,
|
|
497
|
-
dist
|
|
501
|
+
dist: finalDist,
|
|
502
|
+
hasDistData: finalHasDist,
|
|
498
503
|
};
|
|
499
504
|
});
|
|
505
|
+
// Persist the last valid distribution so that data() can fall back
|
|
506
|
+
// to it while api.state.part() is re-hydrating after a view switch.
|
|
507
|
+
createEffect(() => {
|
|
508
|
+
const d = data();
|
|
509
|
+
if (d.hasDistData) {
|
|
510
|
+
setLastDist({ ...d.dist });
|
|
511
|
+
setLastHasDist(true);
|
|
512
|
+
// Also persist across component remounts (view switches)
|
|
513
|
+
try {
|
|
514
|
+
props.api.kv.set(`${KV_PREFIX}.dist_snapshot`, { ...d.dist });
|
|
515
|
+
}
|
|
516
|
+
catch { }
|
|
517
|
+
}
|
|
518
|
+
});
|
|
500
519
|
// ── token distribution (in-process via api.state.part) ──
|
|
501
520
|
const [partVersion, setPartVersion] = createSignal(0);
|
|
502
521
|
// Persist fold state to api.kv
|
|
@@ -508,7 +527,10 @@ function TokenCachePanel(props) {
|
|
|
508
527
|
catch { }
|
|
509
528
|
};
|
|
510
529
|
onMount(() => {
|
|
511
|
-
//
|
|
530
|
+
// Reset panelWidth on (re)mount so the layout uses a clean
|
|
531
|
+
// default until onSizeChange measures the live box dimensions.
|
|
532
|
+
setPanelWidth(DEFAULT_PANEL_WIDTH);
|
|
533
|
+
// Restore fold state from persisted storage (non-critical — fire and forget)
|
|
512
534
|
try {
|
|
513
535
|
setOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.open`, false)));
|
|
514
536
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)));
|
|
@@ -516,19 +538,51 @@ function TokenCachePanel(props) {
|
|
|
516
538
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
|
|
517
539
|
}
|
|
518
540
|
catch { }
|
|
519
|
-
// Restore user config (currency, rate, section visibility)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
541
|
+
// Restore user config (currency, rate, section visibility).
|
|
542
|
+
// Try synchronously first (kv is usually ready on mount), fall back to
|
|
543
|
+
// polling if the module was reloaded and kv hasn't initialised yet.
|
|
544
|
+
const doRestore = () => {
|
|
545
|
+
try {
|
|
546
|
+
const sym = props.api.kv.get(`${KV_PREFIX}.currency`);
|
|
547
|
+
const rate = props.api.kv.get(`${KV_PREFIX}.rate`);
|
|
548
|
+
if (typeof sym === "string")
|
|
549
|
+
setCurrencySymbol(sym);
|
|
550
|
+
if (typeof rate === "number" && rate > 0)
|
|
551
|
+
setExchangeRate(rate);
|
|
552
|
+
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
553
|
+
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
554
|
+
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
555
|
+
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
556
|
+
setBorderVisible(bv !== false);
|
|
557
|
+
// Restore distribution snapshot so the token distribution block
|
|
558
|
+
// doesn't blank out while api.state.part() re-hydrates.
|
|
559
|
+
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
560
|
+
if (cachedDist) {
|
|
561
|
+
setLastDist(cachedDist);
|
|
562
|
+
setLastHasDist(true);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// kv read failed — signals stay at defaults
|
|
567
|
+
}
|
|
568
|
+
// Re-measure panel width after config signals have settled
|
|
569
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
570
|
+
setPanelWidth(Math.max(MIN_PANEL_WIDTH, boxEl.width));
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
if (props.api.kv.ready) {
|
|
574
|
+
doRestore();
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const pollRestore = () => {
|
|
578
|
+
if (!props.api.kv.ready) {
|
|
579
|
+
setTimeout(pollRestore, 10);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
doRestore();
|
|
583
|
+
};
|
|
584
|
+
pollRestore();
|
|
530
585
|
}
|
|
531
|
-
catch { }
|
|
532
586
|
const unsubPart = props.api.event.on("message.part.updated", () => {
|
|
533
587
|
setPartVersion((v) => v + 1);
|
|
534
588
|
});
|
|
@@ -561,45 +615,72 @@ function TokenCachePanel(props) {
|
|
|
561
615
|
return pal().warning;
|
|
562
616
|
return pal().error;
|
|
563
617
|
});
|
|
564
|
-
|
|
618
|
+
/** Horizontal space eaten by border (1+1 when visible) + padding (2+2 when visible). */
|
|
619
|
+
const gutter = createMemo(() => borderVisible() ? 6 : 0);
|
|
620
|
+
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())));
|
|
565
621
|
function trendLabel(t) {
|
|
566
622
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "");
|
|
567
623
|
}
|
|
568
624
|
const barW = createMemo(() => {
|
|
569
625
|
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 +
|
|
626
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
571
627
|
return Math.max(3, panelWidth() - overhead);
|
|
572
628
|
});
|
|
573
629
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
574
630
|
const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%");
|
|
631
|
+
// When border visibility changes the box dimensions shift, which
|
|
632
|
+
// may not reliably trigger onSizeChange across (re)mount cycles.
|
|
633
|
+
// Force panelWidth to resync with the live box after every change.
|
|
634
|
+
createEffect(() => {
|
|
635
|
+
borderVisible();
|
|
636
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
637
|
+
const w = Math.max(MIN_PANEL_WIDTH, boxEl.width);
|
|
638
|
+
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
639
|
+
}
|
|
640
|
+
});
|
|
575
641
|
// left-align label, right-align value — auto-fill space between
|
|
576
642
|
const justify = (label, value, unit = "") => {
|
|
577
|
-
const gauge = panelWidth() -
|
|
643
|
+
const gauge = panelWidth() - gutter();
|
|
578
644
|
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0);
|
|
579
645
|
const gap = Math.max(1, gauge - used);
|
|
580
646
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
581
647
|
};
|
|
582
|
-
return (_jsxs("box", { border:
|
|
648
|
+
return (_jsxs("box", { border: borderVisible(), ...(borderVisible() ? { borderColor: pal().border } : {}), paddingTop: 0, paddingBottom: 0, paddingLeft: borderVisible() ? 2 : 0, paddingRight: borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
583
649
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
584
650
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
585
651
|
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() -
|
|
652
|
+
}, 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
653
|
}
|
|
588
654
|
// ---------------------------------------------------------------------------
|
|
589
655
|
// Plugin entry
|
|
590
656
|
// ---------------------------------------------------------------------------
|
|
591
|
-
function createSidebarSlot(api) {
|
|
657
|
+
function createSidebarSlot(api, signals) {
|
|
592
658
|
return {
|
|
593
659
|
order: 55,
|
|
594
660
|
slots: {
|
|
595
661
|
sidebar_content(ctx, input) {
|
|
596
|
-
return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id }));
|
|
662
|
+
return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id, signals: signals }));
|
|
597
663
|
},
|
|
598
664
|
},
|
|
599
665
|
};
|
|
600
666
|
}
|
|
601
667
|
const tui = async (api) => {
|
|
602
|
-
|
|
668
|
+
// ── shared panel signals ──────────────────────────────────────
|
|
669
|
+
const [currencySymbol, setCurrencySymbol] = createSignal("$");
|
|
670
|
+
const [exchangeRate, setExchangeRate] = createSignal(1);
|
|
671
|
+
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
672
|
+
const [sectionModel, setSectionModel] = createSignal(true);
|
|
673
|
+
const [sectionDist, setSectionDist] = createSignal(true);
|
|
674
|
+
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
675
|
+
const signals = {
|
|
676
|
+
currencySymbol, setCurrencySymbol,
|
|
677
|
+
exchangeRate, setExchangeRate,
|
|
678
|
+
sectionDetail, setSectionDetail,
|
|
679
|
+
sectionModel, setSectionModel,
|
|
680
|
+
sectionDist, setSectionDist,
|
|
681
|
+
borderVisible, setBorderVisible,
|
|
682
|
+
};
|
|
683
|
+
api.slots.register(createSidebarSlot(api, signals));
|
|
603
684
|
// ── slash commands for runtime config ──
|
|
604
685
|
const KV_PREFIX = "cache_panel";
|
|
605
686
|
api.command?.register(() => [
|
|
@@ -617,8 +698,8 @@ const tui = async (api) => {
|
|
|
617
698
|
const defRate = DEFAULT_RATES[opt.value] ?? 1;
|
|
618
699
|
api.kv.set(`${KV_PREFIX}.currency`, sym);
|
|
619
700
|
api.kv.set(`${KV_PREFIX}.rate`, defRate);
|
|
620
|
-
setCurrencySymbol(sym);
|
|
621
|
-
setExchangeRate(defRate);
|
|
701
|
+
signals.setCurrencySymbol(sym);
|
|
702
|
+
signals.setExchangeRate(defRate);
|
|
622
703
|
api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
|
|
623
704
|
dialog?.clear();
|
|
624
705
|
} })));
|
|
@@ -634,7 +715,7 @@ const tui = async (api) => {
|
|
|
634
715
|
const n = parseFloat(val);
|
|
635
716
|
if (n > 0) {
|
|
636
717
|
api.kv.set(`${KV_PREFIX}.rate`, n);
|
|
637
|
-
setExchangeRate(n);
|
|
718
|
+
signals.setExchangeRate(n);
|
|
638
719
|
api.ui.toast({ message: `Exchange rate set to ${n}` });
|
|
639
720
|
}
|
|
640
721
|
dialog?.clear();
|
|
@@ -650,21 +731,31 @@ const tui = async (api) => {
|
|
|
650
731
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
651
732
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
652
733
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
734
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
653
735
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
|
|
654
736
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
655
737
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
656
738
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
739
|
+
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
657
740
|
], onSelect: (opt) => {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
741
|
+
if (opt.value === "border") {
|
|
742
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
743
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur);
|
|
744
|
+
signals.setBorderVisible(!cur);
|
|
745
|
+
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` });
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
const key = `${KV_PREFIX}.section.${opt.value}`;
|
|
749
|
+
const cur = Boolean(api.kv.get(key, true));
|
|
750
|
+
api.kv.set(key, !cur);
|
|
751
|
+
if (opt.value === "detail")
|
|
752
|
+
signals.setSectionDetail(!cur);
|
|
753
|
+
if (opt.value === "model")
|
|
754
|
+
signals.setSectionModel(!cur);
|
|
755
|
+
if (opt.value === "dist")
|
|
756
|
+
signals.setSectionDist(!cur);
|
|
757
|
+
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
758
|
+
}
|
|
668
759
|
dialog?.clear();
|
|
669
760
|
} })));
|
|
670
761
|
},
|
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.8";
|
package/src/index.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
FilePart,
|
|
18
18
|
ReasoningPart,
|
|
19
19
|
} from "@opencode-ai/sdk/v2"
|
|
20
|
-
import { createMemo, createSignal, onMount, onCleanup, Show } from "solid-js"
|
|
20
|
+
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
|
|
21
21
|
import { PLUGIN_VERSION } from "./_version"
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -306,13 +306,23 @@ interface TokenDist {
|
|
|
306
306
|
// Sidebar component
|
|
307
307
|
// ---------------------------------------------------------------------------
|
|
308
308
|
|
|
309
|
-
/**
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
/** Signals shared between the TUI component and slash commands.
|
|
310
|
+
* Created in the `tui` function scope so they do not survive module reload —
|
|
311
|
+
* the component re-creates them on mount and restores user config from kv. */
|
|
312
|
+
interface PanelSignals {
|
|
313
|
+
currencySymbol: () => string
|
|
314
|
+
setCurrencySymbol: (v: string) => void
|
|
315
|
+
exchangeRate: () => number
|
|
316
|
+
setExchangeRate: (v: number) => void
|
|
317
|
+
sectionDetail: () => boolean
|
|
318
|
+
setSectionDetail: (v: boolean) => void
|
|
319
|
+
sectionModel: () => boolean
|
|
320
|
+
setSectionModel: (v: boolean) => void
|
|
321
|
+
sectionDist: () => boolean
|
|
322
|
+
setSectionDist: (v: boolean) => void
|
|
323
|
+
borderVisible: () => boolean
|
|
324
|
+
setBorderVisible: (v: boolean) => void
|
|
325
|
+
}
|
|
316
326
|
|
|
317
327
|
const CURRENCIES: Record<string, string> = {
|
|
318
328
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
@@ -325,8 +335,6 @@ const DEFAULT_RATES: Record<string, number> = {
|
|
|
325
335
|
|
|
326
336
|
const MIN_PANEL_WIDTH = 20
|
|
327
337
|
const DEFAULT_PANEL_WIDTH = 26
|
|
328
|
-
/** Horizontal space eaten by border (1+1) + padding (2+2). */
|
|
329
|
-
const GUTTER = 2 + 4
|
|
330
338
|
|
|
331
339
|
/** ── layout measurement constants (visual columns) ── */
|
|
332
340
|
const LABEL_GAP = 1 // label(如 "Hit")后面的空格
|
|
@@ -341,6 +349,7 @@ function TokenCachePanel(props: {
|
|
|
341
349
|
theme: TuiThemeCurrent
|
|
342
350
|
api: TuiPluginApi
|
|
343
351
|
sessionId: string
|
|
352
|
+
signals: PanelSignals
|
|
344
353
|
}): JSX.Element {
|
|
345
354
|
const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH)
|
|
346
355
|
const [open, setOpen] = createSignal(true)
|
|
@@ -349,9 +358,31 @@ function TokenCachePanel(props: {
|
|
|
349
358
|
const [distOpen, setDistOpen] = createSignal(false)
|
|
350
359
|
let boxEl: any
|
|
351
360
|
|
|
361
|
+
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
362
|
+
const {
|
|
363
|
+
currencySymbol, setCurrencySymbol,
|
|
364
|
+
exchangeRate, setExchangeRate,
|
|
365
|
+
sectionDetail, setSectionDetail,
|
|
366
|
+
sectionModel, setSectionModel,
|
|
367
|
+
sectionDist, setSectionDist,
|
|
368
|
+
borderVisible, setBorderVisible,
|
|
369
|
+
} = props.signals
|
|
370
|
+
|
|
352
371
|
// ── scan session messages reactively ──
|
|
353
372
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
354
373
|
// api.state.session state changes — no event listener needed.
|
|
374
|
+
|
|
375
|
+
// ── distribution cache ────────────────────────────────────────
|
|
376
|
+
// When data() re-computes before api.state.part() is warm (e.g. after
|
|
377
|
+
// a view switch), hasDistData flips to false and the distribution
|
|
378
|
+
// block disappears. Keep the last valid snapshot so the UI stays
|
|
379
|
+
// stable until the next successful computation arrives.
|
|
380
|
+
const [lastDist, setLastDist] = createSignal<TokenDist>({
|
|
381
|
+
system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
|
|
382
|
+
output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
|
|
383
|
+
})
|
|
384
|
+
const [lastHasDist, setLastHasDist] = createSignal(false)
|
|
385
|
+
|
|
355
386
|
const data = createMemo(() => {
|
|
356
387
|
const msgs = props.api.state.session.messages(props.sessionId) as Message[]
|
|
357
388
|
|
|
@@ -518,6 +549,11 @@ function TokenCachePanel(props: {
|
|
|
518
549
|
// Graceful degradation — dist stays at zeroes
|
|
519
550
|
}
|
|
520
551
|
|
|
552
|
+
// Fall back to last known-good distribution while api.state.part()
|
|
553
|
+
// is re-hydrating after a view switch.
|
|
554
|
+
const finalDist = hasDistData ? dist : lastDist()
|
|
555
|
+
const finalHasDist = hasDistData || lastHasDist()
|
|
556
|
+
|
|
521
557
|
return {
|
|
522
558
|
hitRate, read, write, freshInput: input, output,
|
|
523
559
|
cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
|
|
@@ -525,7 +561,20 @@ function TokenCachePanel(props: {
|
|
|
525
561
|
trend, hasTrendData,
|
|
526
562
|
providerName,
|
|
527
563
|
sessionHitRate,
|
|
528
|
-
dist
|
|
564
|
+
dist: finalDist,
|
|
565
|
+
hasDistData: finalHasDist,
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// Persist the last valid distribution so that data() can fall back
|
|
570
|
+
// to it while api.state.part() is re-hydrating after a view switch.
|
|
571
|
+
createEffect(() => {
|
|
572
|
+
const d = data()
|
|
573
|
+
if (d.hasDistData) {
|
|
574
|
+
setLastDist({ ...d.dist })
|
|
575
|
+
setLastHasDist(true)
|
|
576
|
+
// Also persist across component remounts (view switches)
|
|
577
|
+
try { props.api.kv.set(`${KV_PREFIX}.dist_snapshot`, { ...d.dist }) } catch {}
|
|
529
578
|
}
|
|
530
579
|
})
|
|
531
580
|
|
|
@@ -539,7 +588,11 @@ function TokenCachePanel(props: {
|
|
|
539
588
|
}
|
|
540
589
|
|
|
541
590
|
onMount(() => {
|
|
542
|
-
//
|
|
591
|
+
// Reset panelWidth on (re)mount so the layout uses a clean
|
|
592
|
+
// default until onSizeChange measures the live box dimensions.
|
|
593
|
+
setPanelWidth(DEFAULT_PANEL_WIDTH)
|
|
594
|
+
|
|
595
|
+
// Restore fold state from persisted storage (non-critical — fire and forget)
|
|
543
596
|
try {
|
|
544
597
|
setOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.open`, false)))
|
|
545
598
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)))
|
|
@@ -547,16 +600,48 @@ function TokenCachePanel(props: {
|
|
|
547
600
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
|
|
548
601
|
} catch {}
|
|
549
602
|
|
|
550
|
-
// Restore user config (currency, rate, section visibility)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
603
|
+
// Restore user config (currency, rate, section visibility).
|
|
604
|
+
// Try synchronously first (kv is usually ready on mount), fall back to
|
|
605
|
+
// polling if the module was reloaded and kv hasn't initialised yet.
|
|
606
|
+
const doRestore = () => {
|
|
607
|
+
try {
|
|
608
|
+
const sym = props.api.kv.get<string>(`${KV_PREFIX}.currency`)
|
|
609
|
+
const rate = props.api.kv.get<number>(`${KV_PREFIX}.rate`)
|
|
610
|
+
if (typeof sym === "string") setCurrencySymbol(sym)
|
|
611
|
+
if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
|
|
612
|
+
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
613
|
+
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
614
|
+
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
615
|
+
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
616
|
+
setBorderVisible(bv !== false)
|
|
617
|
+
// Restore distribution snapshot so the token distribution block
|
|
618
|
+
// doesn't blank out while api.state.part() re-hydrates.
|
|
619
|
+
const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
|
|
620
|
+
if (cachedDist) {
|
|
621
|
+
setLastDist(cachedDist)
|
|
622
|
+
setLastHasDist(true)
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
// kv read failed — signals stay at defaults
|
|
626
|
+
}
|
|
627
|
+
// Re-measure panel width after config signals have settled
|
|
628
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
629
|
+
setPanelWidth(Math.max(MIN_PANEL_WIDTH, boxEl.width))
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (props.api.kv.ready) {
|
|
634
|
+
doRestore()
|
|
635
|
+
} else {
|
|
636
|
+
const pollRestore = () => {
|
|
637
|
+
if (!props.api.kv.ready) {
|
|
638
|
+
setTimeout(pollRestore, 10)
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
doRestore()
|
|
642
|
+
}
|
|
643
|
+
pollRestore()
|
|
644
|
+
}
|
|
560
645
|
|
|
561
646
|
const unsubPart = props.api.event.on("message.part.updated", () => {
|
|
562
647
|
setPartVersion((v) => v + 1)
|
|
@@ -591,22 +676,36 @@ function TokenCachePanel(props: {
|
|
|
591
676
|
return pal().error
|
|
592
677
|
})
|
|
593
678
|
|
|
594
|
-
|
|
679
|
+
/** Horizontal space eaten by border (1+1 when visible) + padding (2+2 when visible). */
|
|
680
|
+
const gutter = createMemo(() => borderVisible() ? 6 : 0)
|
|
681
|
+
|
|
682
|
+
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())))
|
|
595
683
|
function trendLabel(t: number): string {
|
|
596
684
|
return (t > 0 ? "\u2191" : t < 0 ? "\u2193" : "-") + (t !== 0 ? Math.abs(t).toFixed(1) + "%" : "")
|
|
597
685
|
}
|
|
598
686
|
|
|
599
687
|
const barW = createMemo(() => {
|
|
600
688
|
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 +
|
|
689
|
+
const overhead = visualWidth(T.hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
602
690
|
return Math.max(3, panelWidth() - overhead)
|
|
603
691
|
})
|
|
604
692
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
605
693
|
const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%")
|
|
606
694
|
|
|
695
|
+
// When border visibility changes the box dimensions shift, which
|
|
696
|
+
// may not reliably trigger onSizeChange across (re)mount cycles.
|
|
697
|
+
// Force panelWidth to resync with the live box after every change.
|
|
698
|
+
createEffect(() => {
|
|
699
|
+
borderVisible()
|
|
700
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
701
|
+
const w = Math.max(MIN_PANEL_WIDTH, boxEl.width)
|
|
702
|
+
setPanelWidth((prev) => (prev === w ? prev : w))
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
|
|
607
706
|
// left-align label, right-align value — auto-fill space between
|
|
608
707
|
const justify = (label: string, value: string, unit = ""): string => {
|
|
609
|
-
const gauge = panelWidth() -
|
|
708
|
+
const gauge = panelWidth() - gutter()
|
|
610
709
|
const used = visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
|
|
611
710
|
const gap = Math.max(1, gauge - used)
|
|
612
711
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "")
|
|
@@ -614,12 +713,12 @@ function TokenCachePanel(props: {
|
|
|
614
713
|
|
|
615
714
|
return (
|
|
616
715
|
<box
|
|
617
|
-
border
|
|
618
|
-
borderColor
|
|
716
|
+
border={borderVisible()}
|
|
717
|
+
{...(borderVisible() ? { borderColor: pal().border } : {})}
|
|
619
718
|
paddingTop={0}
|
|
620
719
|
paddingBottom={0}
|
|
621
|
-
paddingLeft={2}
|
|
622
|
-
paddingRight={2}
|
|
720
|
+
paddingLeft={borderVisible() ? 2 : 0}
|
|
721
|
+
paddingRight={borderVisible() ? 2 : 0}
|
|
623
722
|
flexDirection="column"
|
|
624
723
|
gap={0}
|
|
625
724
|
ref={boxEl}
|
|
@@ -641,7 +740,7 @@ function TokenCachePanel(props: {
|
|
|
641
740
|
<Show when={!open() && data().hasData}>
|
|
642
741
|
<Show when={data().hasTrendData}>
|
|
643
742
|
<span>
|
|
644
|
-
{" ".repeat(Math.max(1, panelWidth() -
|
|
743
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
|
|
645
744
|
</span>
|
|
646
745
|
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
647
746
|
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
@@ -650,7 +749,7 @@ function TokenCachePanel(props: {
|
|
|
650
749
|
</Show>
|
|
651
750
|
<Show when={!data().hasTrendData}>
|
|
652
751
|
<span>
|
|
653
|
-
{" ".repeat(Math.max(1, panelWidth() -
|
|
752
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
|
|
654
753
|
</span>
|
|
655
754
|
<span style={{ fg: hitColor() }}>{pct()} {T.hitFolded}</span>
|
|
656
755
|
</Show>
|
|
@@ -714,7 +813,7 @@ function TokenCachePanel(props: {
|
|
|
714
813
|
<Show when={data().saved > 0}>
|
|
715
814
|
<text>
|
|
716
815
|
<span style={{ fg: pal().muted }}>{T.saved}</span>
|
|
717
|
-
<span>{" ".repeat(Math.max(1, panelWidth() -
|
|
816
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
|
|
718
817
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
719
818
|
</text>
|
|
720
819
|
</Show>
|
|
@@ -809,7 +908,7 @@ function TokenCachePanel(props: {
|
|
|
809
908
|
// Plugin entry
|
|
810
909
|
// ---------------------------------------------------------------------------
|
|
811
910
|
|
|
812
|
-
function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
911
|
+
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
813
912
|
return {
|
|
814
913
|
order: 55,
|
|
815
914
|
slots: {
|
|
@@ -819,6 +918,7 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
819
918
|
theme={ctx.theme.current}
|
|
820
919
|
api={api}
|
|
821
920
|
sessionId={input.session_id}
|
|
921
|
+
signals={signals}
|
|
822
922
|
/>
|
|
823
923
|
)
|
|
824
924
|
},
|
|
@@ -827,7 +927,24 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
827
927
|
}
|
|
828
928
|
|
|
829
929
|
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
830
|
-
|
|
930
|
+
// ── shared panel signals ──────────────────────────────────────
|
|
931
|
+
const [currencySymbol, setCurrencySymbol] = createSignal("$")
|
|
932
|
+
const [exchangeRate, setExchangeRate] = createSignal(1)
|
|
933
|
+
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
934
|
+
const [sectionModel, setSectionModel] = createSignal(true)
|
|
935
|
+
const [sectionDist, setSectionDist] = createSignal(true)
|
|
936
|
+
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
937
|
+
|
|
938
|
+
const signals: PanelSignals = {
|
|
939
|
+
currencySymbol, setCurrencySymbol,
|
|
940
|
+
exchangeRate, setExchangeRate,
|
|
941
|
+
sectionDetail, setSectionDetail,
|
|
942
|
+
sectionModel, setSectionModel,
|
|
943
|
+
sectionDist, setSectionDist,
|
|
944
|
+
borderVisible, setBorderVisible,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
api.slots.register(createSidebarSlot(api, signals))
|
|
831
948
|
|
|
832
949
|
// ── slash commands for runtime config ──
|
|
833
950
|
const KV_PREFIX = "cache_panel"
|
|
@@ -850,8 +967,8 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
850
967
|
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
851
968
|
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
852
969
|
api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
853
|
-
setCurrencySymbol(sym)
|
|
854
|
-
setExchangeRate(defRate)
|
|
970
|
+
signals.setCurrencySymbol(sym)
|
|
971
|
+
signals.setExchangeRate(defRate)
|
|
855
972
|
api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` })
|
|
856
973
|
dialog?.clear()
|
|
857
974
|
}}
|
|
@@ -875,7 +992,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
875
992
|
const n = parseFloat(val)
|
|
876
993
|
if (n > 0) {
|
|
877
994
|
api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
878
|
-
setExchangeRate(n)
|
|
995
|
+
signals.setExchangeRate(n)
|
|
879
996
|
api.ui.toast({ message: `Exchange rate set to ${n}` })
|
|
880
997
|
}
|
|
881
998
|
dialog?.clear()
|
|
@@ -893,6 +1010,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
893
1010
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
894
1011
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
895
1012
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1013
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
896
1014
|
dialog?.replace(() => (
|
|
897
1015
|
<api.ui.DialogSelect
|
|
898
1016
|
title="Toggle Section"
|
|
@@ -900,15 +1018,23 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
900
1018
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
901
1019
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
902
1020
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1021
|
+
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
903
1022
|
]}
|
|
904
1023
|
onSelect={(opt) => {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1024
|
+
if (opt.value === "border") {
|
|
1025
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1026
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
1027
|
+
signals.setBorderVisible(!cur)
|
|
1028
|
+
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` })
|
|
1029
|
+
} else {
|
|
1030
|
+
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
1031
|
+
const cur = Boolean(api.kv.get(key, true))
|
|
1032
|
+
api.kv.set(key, !cur)
|
|
1033
|
+
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
1034
|
+
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
1035
|
+
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1036
|
+
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
1037
|
+
}
|
|
912
1038
|
dialog?.clear()
|
|
913
1039
|
}}
|
|
914
1040
|
/>
|