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 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` | 开关区块显示 | 独立控制「Token 明细」「模型与定价」「估算 Token 分布」的显隐 |
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
- 强制英文(Windows PowerShell):
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, or Token Distribution |
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
 
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.6";
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.6";
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
- const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - GUTTER)));
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 + GUTTER;
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() - GUTTER;
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: true, borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: 2, paddingRight: 2, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
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() - 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) })] })] }) })] }) })] }));
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
- 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"}` });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.2.6";
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
- const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - GUTTER)))
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 + GUTTER
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() - GUTTER
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() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend))))}
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() - GUTTER - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded)))}
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() - GUTTER - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
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
- 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"}` })
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
  />