opencode-visual-cache 1.2.10 → 1.2.12
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 +21 -6
- package/README_EN.md +20 -5
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +137 -7
- package/package.json +58 -58
- package/src/_version.ts +1 -1
- package/src/index.tsx +191 -38
package/README.md
CHANGED
|
@@ -44,9 +44,10 @@
|
|
|
44
44
|
- **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
|
|
45
45
|
- **Token 分布**:按角色(系统提示 / 用户 / Agent 指令 / Tool 调用 / Tool 结果)展示估算 Token 占比
|
|
46
46
|
- **折叠记忆**:折叠状态持久化,重启后保持
|
|
47
|
-
-
|
|
47
|
+
- **语言适配**:自动检测系统语言,支持 `/cache-lang` 运行时切换中/英文,偏好持久化
|
|
48
48
|
- **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
|
|
49
|
-
- **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` 动态配置面板
|
|
49
|
+
- **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板
|
|
50
|
+
- **已加载技能**:检测 session 中 LLM 调用 `skill` tool 的记录,展示已加载技能名及估算 Token 占用
|
|
50
51
|
|
|
51
52
|
---
|
|
52
53
|
|
|
@@ -97,8 +98,9 @@ npm install -g opencode-visual-cache@latest
|
|
|
97
98
|
|------|------|---------|
|
|
98
99
|
| `/cache-currency` | 切换货币单位 | 从列表选择货币(USD / CNY / EUR / JPY / GBP / KRW),自动填入默认汇率 |
|
|
99
100
|
| `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 |
|
|
100
|
-
| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 面板边框的显隐 |
|
|
101
|
+
| `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 已加载技能 / 面板边框的显隐 |
|
|
101
102
|
| `/cache-config` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 |
|
|
103
|
+
| `/cache-lang` | 切换显示语言 | 从列表选择中文或 English,界面即时切换,无需重启 |
|
|
102
104
|
|
|
103
105
|
<div align="center">
|
|
104
106
|
<img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="斜杠命令" width="49%"></img>
|
|
@@ -126,11 +128,12 @@ npm install -g opencode-visual-cache@latest
|
|
|
126
128
|
|
|
127
129
|
### 4.3 区块可见性
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
面板中的子区块可以独立关闭,方便在侧边栏空间紧张时隐藏不需要的信息:
|
|
130
132
|
|
|
131
133
|
- **Token 明细**:缓存读 / 缓存写 / 未命中 / 输出
|
|
132
134
|
- **模型与定价**:费用 / 提供商 / 模型名 / 单价
|
|
133
135
|
- **估算 Token 分布**:按角色拆分的 Token 估算
|
|
136
|
+
- **已加载技能**:session 中 LLM 实际调用过的 Skill 名及估算 Token 占用
|
|
134
137
|
|
|
135
138
|
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
136
139
|
|
|
@@ -164,9 +167,17 @@ rm -rf ~/.cache/opencode/packages/opencode-visual-cache@latest
|
|
|
164
167
|
|
|
165
168
|
---
|
|
166
169
|
|
|
167
|
-
## 6.
|
|
170
|
+
## 6. 语言设置
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
插件支持三种方式控制显示语言,按优先级从高到低排列:
|
|
173
|
+
|
|
174
|
+
### 6.1 运行时切换(推荐)
|
|
175
|
+
|
|
176
|
+
在 TUI 中输入 `/cache-lang`,从弹窗选择「中文」或「English」即可即时切换,无需重启。偏好会自动持久化,下次启动自动恢复。
|
|
177
|
+
|
|
178
|
+
### 6.2 环境变量覆盖
|
|
179
|
+
|
|
180
|
+
启动前设置 `CACHE_TUI_LANG` 环境变量可强制指定语言:
|
|
170
181
|
|
|
171
182
|
```powershell
|
|
172
183
|
# Windows PowerShell
|
|
@@ -178,6 +189,10 @@ $env:CACHE_TUI_LANG="en"; opencode
|
|
|
178
189
|
CACHE_TUI_LANG=en opencode
|
|
179
190
|
```
|
|
180
191
|
|
|
192
|
+
### 6.3 自动检测
|
|
193
|
+
|
|
194
|
+
默认自动检测系统语言。若不符合预期,用 `/cache-lang` 手动切换一次即可,偏好会被记住。
|
|
195
|
+
|
|
181
196
|
---
|
|
182
197
|
|
|
183
198
|
## 7. 兼容性
|
package/README_EN.md
CHANGED
|
@@ -44,9 +44,10 @@ If you find this plugin useful, a ⭐ would mean a lot — thank you!<br>
|
|
|
44
44
|
- **Adaptive Colors**: ≥85% green · ≥70% orange · <70% red, auto-desaturated from current theme
|
|
45
45
|
- **Token Distribution**: Per-role (system / user / agent instr / tool call / tool result) estimated token breakdown
|
|
46
46
|
- **Persistent State**: Fold preferences and config remembered across restarts via api.kv
|
|
47
|
-
- **Language**: Auto-detects system locale
|
|
47
|
+
- **Language**: Auto-detects system locale, with `/cache-lang` for runtime switching between Chinese and English — preference persisted across restarts
|
|
48
48
|
- **Multi-currency**: Switch via `/cache-currency` — costs, savings, and per-million rates convert in real time
|
|
49
|
-
- **Slash Commands**: `/cache-rate` `/cache-section` `/cache-config` for live panel configuration
|
|
49
|
+
- **Slash Commands**: `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` for live panel configuration
|
|
50
|
+
- **Loaded Skills**: Detects `skill` tool calls in the session and displays loaded skill names with estimated token footprint
|
|
50
51
|
|
|
51
52
|
---
|
|
52
53
|
|
|
@@ -97,8 +98,9 @@ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime
|
|
|
97
98
|
|---------|----------|------------|
|
|
98
99
|
| `/cache-currency` | Switch currency | Pick from a list (USD / CNY / EUR / JPY / GBP / KRW); default exchange rate auto-filled |
|
|
99
100
|
| `/cache-rate` | Adjust exchange rate | Enter a custom rate (e.g. `7.2` for CNY) |
|
|
100
|
-
| `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, or the panel border |
|
|
101
|
+
| `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, Loaded Skills, or the panel border |
|
|
101
102
|
| `/cache-config` | View current config | Displays currency, rate, and section visibility |
|
|
103
|
+
| `/cache-lang` | Switch display language | Pick Chinese or English from the dialog — takes effect immediately, no restart needed |
|
|
102
104
|
|
|
103
105
|
<div align="center">
|
|
104
106
|
<img src="https://raw.githubusercontent.com/Hotakus/opencode-visual-cache/master/assets/splash_cmd.png" alt="Slash command" width="49%"></img>
|
|
@@ -131,6 +133,7 @@ Three sub-sections can be toggled independently to save sidebar space:
|
|
|
131
133
|
- **Token Detail**: cache read / write / miss / output
|
|
132
134
|
- **Model & Pricing**: cost / provider / model name / per-million rates
|
|
133
135
|
- **Estimated Token Dist.**: per-role token breakdown
|
|
136
|
+
- **Loaded Skills**: skill names the LLM actually loaded via the `skill` tool, with estimated token counts
|
|
134
137
|
|
|
135
138
|
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
139
|
|
|
@@ -162,9 +165,17 @@ Press **`Ctrl + P`** in OpenCode → `install plugin` → `opencode-visual-cache
|
|
|
162
165
|
|
|
163
166
|
---
|
|
164
167
|
|
|
165
|
-
## 6.
|
|
168
|
+
## 6. Language Settings
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
The plugin provides three ways to control the display language, listed by priority (highest first):
|
|
171
|
+
|
|
172
|
+
### 6.1 Runtime Switching (recommended)
|
|
173
|
+
|
|
174
|
+
Type `/cache-lang` in the TUI and select **Chinese** or **English** from the dialog. The panel switches immediately without restarting, and your preference is persisted automatically for the next session.
|
|
175
|
+
|
|
176
|
+
### 6.2 Environment Variable Override
|
|
177
|
+
|
|
178
|
+
Set the `CACHE_TUI_LANG` environment variable before launching to force a specific language:
|
|
168
179
|
|
|
169
180
|
```powershell
|
|
170
181
|
# Windows PowerShell
|
|
@@ -176,6 +187,10 @@ $env:CACHE_TUI_LANG="en"; opencode
|
|
|
176
187
|
CACHE_TUI_LANG=en opencode
|
|
177
188
|
```
|
|
178
189
|
|
|
190
|
+
### 6.3 Auto Detection
|
|
191
|
+
|
|
192
|
+
Defaults to the system locale automatically. If it doesn't match, switch once with `/cache-lang` and the preference will be remembered.
|
|
193
|
+
|
|
179
194
|
---
|
|
180
195
|
|
|
181
196
|
## 7. Compatibility
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.2.12";
|
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.12";
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,22 @@ function visualPadEnd(s, cols) {
|
|
|
35
35
|
const pad = cols - visualWidth(s);
|
|
36
36
|
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
37
37
|
}
|
|
38
|
+
/** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
|
|
39
|
+
function truncateVisual(s, maxCols) {
|
|
40
|
+
if (visualWidth(s) <= maxCols)
|
|
41
|
+
return s;
|
|
42
|
+
let result = "", w = 0;
|
|
43
|
+
for (const c of s) {
|
|
44
|
+
const cw = charColumns(c);
|
|
45
|
+
if (w + cw > maxCols - 1) {
|
|
46
|
+
result += "\u2026";
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
result += c;
|
|
50
|
+
w += cw;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
38
54
|
// ── language override (env: CACHE_TUI_LANG) ──
|
|
39
55
|
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
|
|
40
56
|
// ── language ──────────────────────────────────────────────────────
|
|
@@ -48,7 +64,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
48
64
|
return false;
|
|
49
65
|
}
|
|
50
66
|
})();
|
|
51
|
-
const
|
|
67
|
+
const ZH_T = {
|
|
52
68
|
title: "缓存统计",
|
|
53
69
|
hit: "命中率",
|
|
54
70
|
totalHit: "总命中:",
|
|
@@ -77,7 +93,9 @@ const T = LANG_ZH ? {
|
|
|
77
93
|
distOut: "输出:",
|
|
78
94
|
secDetail: "明细",
|
|
79
95
|
secModel: "模型",
|
|
80
|
-
|
|
96
|
+
secSkills: "已加载技能",
|
|
97
|
+
};
|
|
98
|
+
const EN_T = {
|
|
81
99
|
title: "Token Cache",
|
|
82
100
|
hit: "Hit",
|
|
83
101
|
totalHit: "Total Hit:",
|
|
@@ -106,6 +124,7 @@ const T = LANG_ZH ? {
|
|
|
106
124
|
distOut: "Output:",
|
|
107
125
|
secDetail: "Detail",
|
|
108
126
|
secModel: "Model",
|
|
127
|
+
secSkills: "Loaded Skills",
|
|
109
128
|
};
|
|
110
129
|
// ── color helpers ────────────────────────────────────────────────
|
|
111
130
|
/** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
|
|
@@ -293,9 +312,12 @@ function TokenCachePanel(props) {
|
|
|
293
312
|
const [detailOpen, setDetailOpen] = createSignal(true);
|
|
294
313
|
const [modelOpen, setModelOpen] = createSignal(true);
|
|
295
314
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
315
|
+
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
296
316
|
let boxEl;
|
|
297
317
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
298
|
-
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, borderVisible, setBorderVisible, } = props.signals;
|
|
318
|
+
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, borderVisible, setBorderVisible, } = props.signals;
|
|
319
|
+
// ── reactive translation (follows langZH signal) ──
|
|
320
|
+
const t = createMemo(() => langZH() ? ZH_T : EN_T);
|
|
299
321
|
// ── scan session messages reactively ──
|
|
300
322
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
301
323
|
// api.state.session state changes — no event listener needed.
|
|
@@ -316,6 +338,8 @@ function TokenCachePanel(props) {
|
|
|
316
338
|
providerName: "", sessionHitRate: 0,
|
|
317
339
|
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
318
340
|
hasDistData: false,
|
|
341
|
+
skills: [],
|
|
342
|
+
hasSkills: false,
|
|
319
343
|
});
|
|
320
344
|
const [refreshTick, setRefreshTick] = createSignal(0);
|
|
321
345
|
createEffect(() => {
|
|
@@ -371,6 +395,7 @@ function TokenCachePanel(props) {
|
|
|
371
395
|
const distData = untrack(() => {
|
|
372
396
|
let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
|
|
373
397
|
let hasDistData = false;
|
|
398
|
+
const loadedSkills = new Map();
|
|
374
399
|
try {
|
|
375
400
|
const session = props.api.state.session.get(sid), cfg = props.api.state.config;
|
|
376
401
|
const agentName = String(session?.agent ?? cfg?.default_agent ?? "build");
|
|
@@ -428,6 +453,25 @@ function TokenCachePanel(props) {
|
|
|
428
453
|
if (e.error)
|
|
429
454
|
dist.toolResult += estimateTokens(e.error);
|
|
430
455
|
}
|
|
456
|
+
if (tp.tool === "skill" && tp.state.status === "completed") {
|
|
457
|
+
// TUI SDK strips tool metadata — extract skill name from well-known output format.
|
|
458
|
+
// Cross-validated against api.client.app.skills() when available.
|
|
459
|
+
let name = tp.state.metadata?.name;
|
|
460
|
+
if (typeof name !== "string") {
|
|
461
|
+
const m = typeof tp.state.output === "string"
|
|
462
|
+
? tp.state.output.match(/^#{1,2}\s*Skill:\s*(.+)/m)
|
|
463
|
+
: null;
|
|
464
|
+
if (m)
|
|
465
|
+
name = m[1].trim();
|
|
466
|
+
}
|
|
467
|
+
if (typeof name === "string") {
|
|
468
|
+
const tokens = typeof tp.state.output === "string" ? estimateTokens(tp.state.output) : 0;
|
|
469
|
+
const existing = loadedSkills.get(name);
|
|
470
|
+
if (!existing || existing.tokens < tokens) {
|
|
471
|
+
loadedSkills.set(name, { name, tokens });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
431
475
|
}
|
|
432
476
|
else if (p.type === "reasoning")
|
|
433
477
|
dist.agent += estimateTokens(p.text);
|
|
@@ -455,7 +499,8 @@ function TokenCachePanel(props) {
|
|
|
455
499
|
}
|
|
456
500
|
catch { }
|
|
457
501
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
458
|
-
|
|
502
|
+
const skills = [...loadedSkills.values()];
|
|
503
|
+
return { finalDist, finalHasDist, skills };
|
|
459
504
|
});
|
|
460
505
|
setDataSignal({
|
|
461
506
|
hitRate, read, write, freshInput: input, output, cost, saved, model,
|
|
@@ -463,6 +508,7 @@ function TokenCachePanel(props) {
|
|
|
463
508
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
464
509
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
465
510
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
511
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
466
512
|
});
|
|
467
513
|
});
|
|
468
514
|
const data = createMemo(() => {
|
|
@@ -502,6 +548,7 @@ function TokenCachePanel(props) {
|
|
|
502
548
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)));
|
|
503
549
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)));
|
|
504
550
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
|
|
551
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)));
|
|
505
552
|
}
|
|
506
553
|
catch { }
|
|
507
554
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -518,8 +565,14 @@ function TokenCachePanel(props) {
|
|
|
518
565
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
519
566
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
520
567
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
568
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)));
|
|
521
569
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
522
570
|
setBorderVisible(bv !== false);
|
|
571
|
+
// Restore language preference
|
|
572
|
+
const savedLang = props.api.kv.get(`${KV_PREFIX}.lang`);
|
|
573
|
+
if (savedLang === "zh" || savedLang === "en") {
|
|
574
|
+
setLangZH(savedLang === "zh");
|
|
575
|
+
}
|
|
523
576
|
// Restore distribution snapshot so the token distribution block
|
|
524
577
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
525
578
|
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
@@ -603,7 +656,7 @@ function TokenCachePanel(props) {
|
|
|
603
656
|
}
|
|
604
657
|
const barW = createMemo(() => {
|
|
605
658
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
606
|
-
const overhead = visualWidth(
|
|
659
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
607
660
|
return Math.max(3, panelWidth() - overhead);
|
|
608
661
|
});
|
|
609
662
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
@@ -629,7 +682,12 @@ function TokenCachePanel(props) {
|
|
|
629
682
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
630
683
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
631
684
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
632
|
-
}, 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:
|
|
685
|
+
}, 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.apiInput), t().tok) })] })] }) }), _jsx(Show, { when: sectionSkills(), children: _jsxs(Show, { when: data().hasSkills, children: [_jsxs("text", { onMouseUp: () => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: skillsOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secSkills }) }), _jsxs("span", { style: { fg: pal().muted }, children: [" (", data().skills.length, ")"] }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t().secSkills + ` (${data().skills.length})`)) })] }), _jsx(Show, { when: skillsOpen(), children: data().skills.map((sk) => {
|
|
686
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok);
|
|
687
|
+
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
688
|
+
const label = truncateVisual(sk.name, maxLabel);
|
|
689
|
+
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t().tok) }));
|
|
690
|
+
}) })] }) })] }) })] }));
|
|
633
691
|
}
|
|
634
692
|
// ---------------------------------------------------------------------------
|
|
635
693
|
// Plugin entry
|
|
@@ -651,13 +709,17 @@ const tui = async (api) => {
|
|
|
651
709
|
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
652
710
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
653
711
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
712
|
+
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
654
713
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
714
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
655
715
|
const signals = {
|
|
656
716
|
currencySymbol, setCurrencySymbol,
|
|
657
717
|
exchangeRate, setExchangeRate,
|
|
718
|
+
langZH, setLangZH,
|
|
658
719
|
sectionDetail, setSectionDetail,
|
|
659
720
|
sectionModel, setSectionModel,
|
|
660
721
|
sectionDist, setSectionDist,
|
|
722
|
+
sectionSkills, setSectionSkills,
|
|
661
723
|
borderVisible, setBorderVisible,
|
|
662
724
|
};
|
|
663
725
|
api.slots.register(createSidebarSlot(api, signals));
|
|
@@ -711,11 +773,13 @@ const tui = async (api) => {
|
|
|
711
773
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
712
774
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
713
775
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
776
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
714
777
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
715
778
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
|
|
716
779
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
717
780
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
718
781
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
782
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
719
783
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
720
784
|
], onSelect: (opt) => {
|
|
721
785
|
if (opt.value === "border") {
|
|
@@ -734,6 +798,8 @@ const tui = async (api) => {
|
|
|
734
798
|
signals.setSectionModel(!cur);
|
|
735
799
|
if (opt.value === "dist")
|
|
736
800
|
signals.setSectionDist(!cur);
|
|
801
|
+
if (opt.value === "skills")
|
|
802
|
+
signals.setSectionSkills(!cur);
|
|
737
803
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
738
804
|
}
|
|
739
805
|
dialog?.clear();
|
|
@@ -751,14 +817,78 @@ const tui = async (api) => {
|
|
|
751
817
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
752
818
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
753
819
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
820
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
754
821
|
api.ui.toast({
|
|
755
822
|
title: "Cache Panel Config",
|
|
756
|
-
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
|
|
823
|
+
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
|
|
757
824
|
duration: 8000,
|
|
758
825
|
});
|
|
759
826
|
dialog?.clear();
|
|
760
827
|
},
|
|
761
828
|
},
|
|
829
|
+
{
|
|
830
|
+
title: "Cache: Switch Language",
|
|
831
|
+
value: "cache.lang",
|
|
832
|
+
description: "Switch between Chinese and English display",
|
|
833
|
+
slash: { name: "cache-lang" },
|
|
834
|
+
onSelect: (dialog) => {
|
|
835
|
+
const cur = langZH();
|
|
836
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Display Language", options: [
|
|
837
|
+
{ title: `中文 ${cur ? "\u2713" : ""}`, value: "zh" },
|
|
838
|
+
{ title: `English ${cur ? "" : "\u2713"}`, value: "en" },
|
|
839
|
+
], onSelect: (opt) => {
|
|
840
|
+
const zh = opt.value === "zh";
|
|
841
|
+
api.kv.set(`${KV_PREFIX}.lang`, opt.value);
|
|
842
|
+
setLangZH(zh);
|
|
843
|
+
api.ui.toast({ message: zh ? "语言已切换为中文" : "Switched to English" });
|
|
844
|
+
dialog?.clear();
|
|
845
|
+
} })));
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
title: "Cache: Debug Skills Detection",
|
|
850
|
+
value: "cache.debug-skills",
|
|
851
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
852
|
+
slash: { name: "cache-debug-skills" },
|
|
853
|
+
onSelect: () => {
|
|
854
|
+
const rt = api.route.current;
|
|
855
|
+
if (rt.name !== "session" || !rt.params) {
|
|
856
|
+
api.ui.toast({ message: "Please run this command inside a session", variant: "warning" });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const sid = String(rt.params.sessionID);
|
|
860
|
+
const msgs = api.state.session.messages(sid);
|
|
861
|
+
const byTool = {};
|
|
862
|
+
const skillParts = [];
|
|
863
|
+
for (const msg of msgs) {
|
|
864
|
+
if (msg.role !== "assistant")
|
|
865
|
+
continue;
|
|
866
|
+
let parts = [];
|
|
867
|
+
try {
|
|
868
|
+
parts = api.state.part(msg.id);
|
|
869
|
+
}
|
|
870
|
+
catch { }
|
|
871
|
+
for (const p of parts) {
|
|
872
|
+
if (p.type === "tool") {
|
|
873
|
+
const t = String(p.tool ?? "?");
|
|
874
|
+
byTool[t] = (byTool[t] ?? 0) + 1;
|
|
875
|
+
if (t === "skill") {
|
|
876
|
+
const meta = p.state?.metadata;
|
|
877
|
+
const rootMeta = p.metadata;
|
|
878
|
+
skillParts.push(`state.metadata=${JSON.stringify(meta)} | root.metadata=${JSON.stringify(rootMeta)} | state.title="${p.state?.title}" | state.output[:80]="${String(p.state?.output ?? "").slice(0, 80)}"`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ");
|
|
884
|
+
const extra = skillParts.length > 0 ? "\n\nSkill parts:\n" + skillParts.join("\n") : "\n\n⚠ No skill tool parts found — AI may be reading SKILL.md instead. Try: 'Use the skill tool to load karpathy-guidelines'";
|
|
885
|
+
api.ui.toast({
|
|
886
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
887
|
+
message: summary + extra,
|
|
888
|
+
duration: 15000,
|
|
889
|
+
});
|
|
890
|
+
},
|
|
891
|
+
},
|
|
762
892
|
]);
|
|
763
893
|
};
|
|
764
894
|
const mod = {
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opencode-visual-cache",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./dist/index.js",
|
|
11
|
-
"types": "./dist/index.d.ts"
|
|
12
|
-
},
|
|
13
|
-
"./tui": {
|
|
14
|
-
"import": "./src/index.tsx",
|
|
15
|
-
"config": {
|
|
16
|
-
"enabled": true
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"src",
|
|
23
|
-
"install.mjs",
|
|
24
|
-
"README.md",
|
|
25
|
-
"README_EN.md"
|
|
26
|
-
],
|
|
27
|
-
"bin": {
|
|
28
|
-
"opencode-visual-cache": "install.mjs"
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "tsc",
|
|
32
|
-
"typecheck": "tsc --noEmit",
|
|
33
|
-
"version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
|
|
34
|
-
"prepublishOnly": "tsc"
|
|
35
|
-
},
|
|
36
|
-
"keywords": [
|
|
37
|
-
"opencode",
|
|
38
|
-
"opencode-plugin",
|
|
39
|
-
"tui",
|
|
40
|
-
"token",
|
|
41
|
-
"cache-hit-rate",
|
|
42
|
-
"cost-tracking"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@opencode-ai/plugin": ">=1.14.0",
|
|
47
|
-
"@opencode-ai/sdk": ">=1.14.0",
|
|
48
|
-
"@opentui/core": ">=0.2.0",
|
|
49
|
-
"@opentui/solid": ">=0.2.0",
|
|
50
|
-
"solid-js": ">=1.9.0"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@opencode-ai/plugin": "^1.14.50",
|
|
54
|
-
"@opencode-ai/sdk": "^1.14.50",
|
|
55
|
-
"tsx": "^4.22.3",
|
|
56
|
-
"typescript": "^5.8.0"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-visual-cache",
|
|
3
|
+
"version": "1.2.12",
|
|
4
|
+
"description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./tui": {
|
|
14
|
+
"import": "./src/index.tsx",
|
|
15
|
+
"config": {
|
|
16
|
+
"enabled": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src",
|
|
23
|
+
"install.mjs",
|
|
24
|
+
"README.md",
|
|
25
|
+
"README_EN.md"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"opencode-visual-cache": "install.mjs"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
|
|
34
|
+
"prepublishOnly": "tsc"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"opencode",
|
|
38
|
+
"opencode-plugin",
|
|
39
|
+
"tui",
|
|
40
|
+
"token",
|
|
41
|
+
"cache-hit-rate",
|
|
42
|
+
"cost-tracking"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@opencode-ai/plugin": ">=1.14.0",
|
|
47
|
+
"@opencode-ai/sdk": ">=1.14.0",
|
|
48
|
+
"@opentui/core": ">=0.2.0",
|
|
49
|
+
"@opentui/solid": ">=0.2.0",
|
|
50
|
+
"solid-js": ">=1.9.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@opencode-ai/plugin": "^1.14.50",
|
|
54
|
+
"@opencode-ai/sdk": "^1.14.50",
|
|
55
|
+
"tsx": "^4.22.3",
|
|
56
|
+
"typescript": "^5.8.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
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.12";
|
package/src/index.tsx
CHANGED
|
@@ -59,6 +59,18 @@ function visualPadEnd(s: string, cols: number): string {
|
|
|
59
59
|
return pad > 0 ? s + " ".repeat(pad) : s
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
|
|
63
|
+
function truncateVisual(s: string, maxCols: number): string {
|
|
64
|
+
if (visualWidth(s) <= maxCols) return s
|
|
65
|
+
let result = "", w = 0
|
|
66
|
+
for (const c of s) {
|
|
67
|
+
const cw = charColumns(c)
|
|
68
|
+
if (w + cw > maxCols - 1) { result += "\u2026"; break }
|
|
69
|
+
result += c; w += cw
|
|
70
|
+
}
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
// ── language override (env: CACHE_TUI_LANG) ──
|
|
63
75
|
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined
|
|
64
76
|
|
|
@@ -71,7 +83,7 @@ const LANG_ZH = DEBUG_LANG
|
|
|
71
83
|
catch { return false }
|
|
72
84
|
})()
|
|
73
85
|
|
|
74
|
-
const
|
|
86
|
+
const ZH_T = {
|
|
75
87
|
title: "缓存统计",
|
|
76
88
|
hit: "命中率",
|
|
77
89
|
totalHit: "总命中:",
|
|
@@ -100,7 +112,10 @@ const T = LANG_ZH ? {
|
|
|
100
112
|
distOut: "输出:",
|
|
101
113
|
secDetail: "明细",
|
|
102
114
|
secModel: "模型",
|
|
103
|
-
|
|
115
|
+
secSkills: "已加载技能",
|
|
116
|
+
} as const
|
|
117
|
+
|
|
118
|
+
const EN_T = {
|
|
104
119
|
title: "Token Cache",
|
|
105
120
|
hit: "Hit",
|
|
106
121
|
totalHit: "Total Hit:",
|
|
@@ -129,6 +144,7 @@ const T = LANG_ZH ? {
|
|
|
129
144
|
distOut: "Output:",
|
|
130
145
|
secDetail: "Detail",
|
|
131
146
|
secModel: "Model",
|
|
147
|
+
secSkills: "Loaded Skills",
|
|
132
148
|
} as const
|
|
133
149
|
|
|
134
150
|
// ── color helpers ────────────────────────────────────────────────
|
|
@@ -317,12 +333,16 @@ interface PanelSignals {
|
|
|
317
333
|
setCurrencySymbol: (v: string) => void
|
|
318
334
|
exchangeRate: () => number
|
|
319
335
|
setExchangeRate: (v: number) => void
|
|
336
|
+
langZH: () => boolean
|
|
337
|
+
setLangZH: (v: boolean) => void
|
|
320
338
|
sectionDetail: () => boolean
|
|
321
339
|
setSectionDetail: (v: boolean) => void
|
|
322
340
|
sectionModel: () => boolean
|
|
323
341
|
setSectionModel: (v: boolean) => void
|
|
324
342
|
sectionDist: () => boolean
|
|
325
343
|
setSectionDist: (v: boolean) => void
|
|
344
|
+
sectionSkills: () => boolean
|
|
345
|
+
setSectionSkills: (v: boolean) => void
|
|
326
346
|
borderVisible: () => boolean
|
|
327
347
|
setBorderVisible: (v: boolean) => void
|
|
328
348
|
}
|
|
@@ -359,18 +379,24 @@ function TokenCachePanel(props: {
|
|
|
359
379
|
const [detailOpen, setDetailOpen] = createSignal(true)
|
|
360
380
|
const [modelOpen, setModelOpen] = createSignal(true)
|
|
361
381
|
const [distOpen, setDistOpen] = createSignal(false)
|
|
382
|
+
const [skillsOpen, setSkillsOpen] = createSignal(true)
|
|
362
383
|
let boxEl: any
|
|
363
384
|
|
|
364
385
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
365
386
|
const {
|
|
366
387
|
currencySymbol, setCurrencySymbol,
|
|
367
388
|
exchangeRate, setExchangeRate,
|
|
389
|
+
langZH, setLangZH,
|
|
368
390
|
sectionDetail, setSectionDetail,
|
|
369
391
|
sectionModel, setSectionModel,
|
|
370
392
|
sectionDist, setSectionDist,
|
|
393
|
+
sectionSkills, setSectionSkills,
|
|
371
394
|
borderVisible, setBorderVisible,
|
|
372
395
|
} = props.signals
|
|
373
396
|
|
|
397
|
+
// ── reactive translation (follows langZH signal) ──
|
|
398
|
+
const t = createMemo(() => langZH() ? ZH_T : EN_T)
|
|
399
|
+
|
|
374
400
|
// ── scan session messages reactively ──
|
|
375
401
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
376
402
|
// api.state.session state changes — no event listener needed.
|
|
@@ -393,6 +419,8 @@ function TokenCachePanel(props: {
|
|
|
393
419
|
providerName: "", sessionHitRate: 0,
|
|
394
420
|
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
395
421
|
hasDistData: false,
|
|
422
|
+
skills: [] as { name: string; tokens: number }[],
|
|
423
|
+
hasSkills: false,
|
|
396
424
|
})
|
|
397
425
|
const [refreshTick, setRefreshTick] = createSignal(0)
|
|
398
426
|
|
|
@@ -432,6 +460,7 @@ function TokenCachePanel(props: {
|
|
|
432
460
|
const distData = untrack(() => {
|
|
433
461
|
let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 }
|
|
434
462
|
let hasDistData = false
|
|
463
|
+
const loadedSkills = new Map<string, { name: string; tokens: number }>()
|
|
435
464
|
try {
|
|
436
465
|
const session = props.api.state.session.get(sid), cfg = props.api.state.config as Record<string, unknown>
|
|
437
466
|
const agentName = String(session?.agent ?? (cfg as any)?.default_agent ?? "build")
|
|
@@ -459,6 +488,24 @@ function TokenCachePanel(props: {
|
|
|
459
488
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
460
489
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
461
490
|
else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
|
|
491
|
+
if (tp.tool === "skill" && tp.state.status === "completed") {
|
|
492
|
+
// TUI SDK strips tool metadata — extract skill name from well-known output format.
|
|
493
|
+
// Cross-validated against api.client.app.skills() when available.
|
|
494
|
+
let name: string | undefined = tp.state.metadata?.name
|
|
495
|
+
if (typeof name !== "string") {
|
|
496
|
+
const m = typeof tp.state.output === "string"
|
|
497
|
+
? tp.state.output.match(/^#{1,2}\s*Skill:\s*(.+)/m)
|
|
498
|
+
: null
|
|
499
|
+
if (m) name = m[1].trim()
|
|
500
|
+
}
|
|
501
|
+
if (typeof name === "string") {
|
|
502
|
+
const tokens = typeof tp.state.output === "string" ? estimateTokens(tp.state.output) : 0
|
|
503
|
+
const existing = loadedSkills.get(name)
|
|
504
|
+
if (!existing || existing.tokens < tokens) {
|
|
505
|
+
loadedSkills.set(name, { name, tokens })
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
462
509
|
} else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
|
|
463
510
|
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
464
511
|
}
|
|
@@ -476,7 +523,8 @@ function TokenCachePanel(props: {
|
|
|
476
523
|
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
|
|
477
524
|
} catch {}
|
|
478
525
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
479
|
-
|
|
526
|
+
const skills = [...loadedSkills.values()]
|
|
527
|
+
return { finalDist, finalHasDist, skills }
|
|
480
528
|
})
|
|
481
529
|
|
|
482
530
|
setDataSignal({
|
|
@@ -485,6 +533,7 @@ function TokenCachePanel(props: {
|
|
|
485
533
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
486
534
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
487
535
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
536
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
488
537
|
})
|
|
489
538
|
})
|
|
490
539
|
|
|
@@ -524,6 +573,7 @@ function TokenCachePanel(props: {
|
|
|
524
573
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)))
|
|
525
574
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)))
|
|
526
575
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
|
|
576
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)))
|
|
527
577
|
} catch {}
|
|
528
578
|
|
|
529
579
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -538,8 +588,14 @@ function TokenCachePanel(props: {
|
|
|
538
588
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
539
589
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
540
590
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
591
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)))
|
|
541
592
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
542
593
|
setBorderVisible(bv !== false)
|
|
594
|
+
// Restore language preference
|
|
595
|
+
const savedLang = props.api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
596
|
+
if (savedLang === "zh" || savedLang === "en") {
|
|
597
|
+
setLangZH(savedLang === "zh")
|
|
598
|
+
}
|
|
543
599
|
// Restore distribution snapshot so the token distribution block
|
|
544
600
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
545
601
|
const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
|
|
@@ -623,7 +679,7 @@ function TokenCachePanel(props: {
|
|
|
623
679
|
|
|
624
680
|
const barW = createMemo(() => {
|
|
625
681
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
626
|
-
const overhead = visualWidth(
|
|
682
|
+
const overhead = visualWidth(t().hit) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
627
683
|
return Math.max(3, panelWidth() - overhead)
|
|
628
684
|
})
|
|
629
685
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
@@ -669,7 +725,7 @@ function TokenCachePanel(props: {
|
|
|
669
725
|
<text onMouseUp={() => setOpen((o) => { const n = !o; persistFold("open", n); return n })}>
|
|
670
726
|
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
671
727
|
<span style={{ fg: pal().primary }}>
|
|
672
|
-
<b>{
|
|
728
|
+
<b>{t().title}</b>
|
|
673
729
|
<Show when={open()}>
|
|
674
730
|
<span style={{ fg: pal().muted }}> (v{PLUGIN_VERSION})</span>
|
|
675
731
|
</Show>
|
|
@@ -677,18 +733,18 @@ function TokenCachePanel(props: {
|
|
|
677
733
|
<Show when={!open() && data().hasData}>
|
|
678
734
|
<Show when={data().hasTrendData}>
|
|
679
735
|
<span>
|
|
680
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(
|
|
736
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded + " " + trendLabel(data().trend))))}
|
|
681
737
|
</span>
|
|
682
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
738
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
683
739
|
<span style={{ fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
684
740
|
{" "}{trendLabel(data().trend)}
|
|
685
741
|
</span>
|
|
686
742
|
</Show>
|
|
687
743
|
<Show when={!data().hasTrendData}>
|
|
688
744
|
<span>
|
|
689
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(
|
|
745
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded)))}
|
|
690
746
|
</span>
|
|
691
|
-
<span style={{ fg: hitColor() }}>{pct()} {
|
|
747
|
+
<span style={{ fg: hitColor() }}>{pct()} {t().hitFolded}</span>
|
|
692
748
|
</Show>
|
|
693
749
|
</Show>
|
|
694
750
|
</text>
|
|
@@ -699,7 +755,7 @@ function TokenCachePanel(props: {
|
|
|
699
755
|
<text fg={pal().muted}>{sep()}</text>
|
|
700
756
|
<text>
|
|
701
757
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
702
|
-
<span style={{ fg: pal().muted }}>{
|
|
758
|
+
<span style={{ fg: pal().muted }}>{t().noData}</span>
|
|
703
759
|
</text>
|
|
704
760
|
</>
|
|
705
761
|
}>
|
|
@@ -707,7 +763,7 @@ function TokenCachePanel(props: {
|
|
|
707
763
|
|
|
708
764
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
709
765
|
<text>
|
|
710
|
-
<span style={{ fg: pal().text }}>{
|
|
766
|
+
<span style={{ fg: pal().text }}>{t().hit} </span>
|
|
711
767
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
712
768
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
713
769
|
<Show when={data().hasTrendData}>
|
|
@@ -719,38 +775,38 @@ function TokenCachePanel(props: {
|
|
|
719
775
|
|
|
720
776
|
{/* session cumulative hit rate */}
|
|
721
777
|
<text fg={pal().muted}>
|
|
722
|
-
{justify(
|
|
778
|
+
{justify(t().totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%")}
|
|
723
779
|
</text>
|
|
724
780
|
|
|
725
781
|
{/* ── detail section (collapsible, default open) ── */}
|
|
726
782
|
<Show when={sectionDetail()}>
|
|
727
783
|
<text onMouseUp={() => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n })}>
|
|
728
784
|
<span style={{ fg: pal().muted }}>{detailOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
729
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
730
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") +
|
|
785
|
+
<span style={{ fg: pal().primary }}><b>{t().secDetail}</b></span>
|
|
786
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t().secDetail))}</span>
|
|
731
787
|
</text>
|
|
732
788
|
|
|
733
789
|
<Show when={detailOpen()}>
|
|
734
790
|
<Show when={data().read > 0}>
|
|
735
791
|
<text fg={pal().muted}>
|
|
736
|
-
{justify(
|
|
792
|
+
{justify(t().read, fmt(data().read), t().tok)}
|
|
737
793
|
</text>
|
|
738
794
|
</Show>
|
|
739
795
|
<Show when={data().write > 0}>
|
|
740
796
|
<text fg={pal().muted}>
|
|
741
|
-
{justify(
|
|
797
|
+
{justify(t().write, fmt(data().write), t().tok)}
|
|
742
798
|
</text>
|
|
743
799
|
</Show>
|
|
744
800
|
<text fg={pal().muted}>
|
|
745
|
-
{justify(
|
|
801
|
+
{justify(t().miss, fmt(data().freshInput), t().tok)}
|
|
746
802
|
</text>
|
|
747
803
|
<text fg={pal().muted}>
|
|
748
|
-
{justify(
|
|
804
|
+
{justify(t().out, fmt(data().output), t().tok)}
|
|
749
805
|
</text>
|
|
750
806
|
<Show when={data().saved > 0}>
|
|
751
807
|
<text>
|
|
752
|
-
<span style={{ fg: pal().muted }}>{
|
|
753
|
-
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(
|
|
808
|
+
<span style={{ fg: pal().muted }}>{t().saved}</span>
|
|
809
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t().saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
|
|
754
810
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
755
811
|
</text>
|
|
756
812
|
</Show>
|
|
@@ -761,34 +817,34 @@ function TokenCachePanel(props: {
|
|
|
761
817
|
<Show when={sectionModel()}>
|
|
762
818
|
{<text onMouseUp={() => setModelOpen((o) => { const n = !o; persistFold("model", n); return n })}>
|
|
763
819
|
<span style={{ fg: pal().muted }}>{modelOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
764
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
765
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") +
|
|
820
|
+
<span style={{ fg: pal().primary }}><b>{t().secModel}</b></span>
|
|
821
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t().secModel))}</span>
|
|
766
822
|
</text>}
|
|
767
823
|
|
|
768
824
|
<Show when={modelOpen()}>
|
|
769
825
|
<text fg={pal().text}>
|
|
770
|
-
{justify(
|
|
826
|
+
{justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate()))}
|
|
771
827
|
</text>
|
|
772
828
|
<Show when={data().providerName}>
|
|
773
829
|
<text fg={pal().muted}>
|
|
774
|
-
{justify(
|
|
830
|
+
{justify(t().provider, data().providerName)}
|
|
775
831
|
</text>
|
|
776
832
|
</Show>
|
|
777
833
|
<text fg={pal().muted}>
|
|
778
|
-
{justify(
|
|
834
|
+
{justify(t().model, data().model)}
|
|
779
835
|
</text>
|
|
780
836
|
<Show when={data().hasPricing}>
|
|
781
837
|
<text fg={pal().muted}>
|
|
782
|
-
{justify(
|
|
838
|
+
{justify(t().rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate)}
|
|
783
839
|
</text>
|
|
784
840
|
<Show when={data().cacheReadRate > 0}>
|
|
785
841
|
<text fg={pal().muted}>
|
|
786
|
-
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " +
|
|
842
|
+
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate)}
|
|
787
843
|
</text>
|
|
788
844
|
</Show>
|
|
789
845
|
<Show when={data().cacheWriteRate > 0}>
|
|
790
846
|
<text fg={pal().muted}>
|
|
791
|
-
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " +
|
|
847
|
+
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate)}
|
|
792
848
|
</text>
|
|
793
849
|
</Show>
|
|
794
850
|
</Show>
|
|
@@ -800,44 +856,68 @@ function TokenCachePanel(props: {
|
|
|
800
856
|
<Show when={data().hasDistData}>
|
|
801
857
|
{<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
|
|
802
858
|
<span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
803
|
-
<span style={{ fg: pal().primary }}><b>{
|
|
804
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") +
|
|
859
|
+
<span style={{ fg: pal().primary }}><b>{t().distTitle}</b></span>
|
|
860
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t().distTitle))}</span>
|
|
805
861
|
</text>}
|
|
806
862
|
<Show when={distOpen()}>
|
|
807
863
|
<Show when={data().dist.system > 0}>
|
|
808
864
|
<text fg={pal().muted}>
|
|
809
|
-
{justify(
|
|
865
|
+
{justify(t().distSys, fmt(data().dist.system), t().tok)}
|
|
810
866
|
</text>
|
|
811
867
|
</Show>
|
|
812
868
|
<Show when={data().dist.user > 0}>
|
|
813
869
|
<text fg={pal().muted}>
|
|
814
|
-
{justify(
|
|
870
|
+
{justify(t().distUser, fmt(data().dist.user), t().tok)}
|
|
815
871
|
</text>
|
|
816
872
|
</Show>
|
|
817
873
|
<Show when={data().dist.agent > 0}>
|
|
818
874
|
<text fg={pal().muted}>
|
|
819
|
-
{justify(
|
|
875
|
+
{justify(t().distAgent, fmt(data().dist.agent), t().tok)}
|
|
820
876
|
</text>
|
|
821
877
|
</Show>
|
|
822
878
|
<Show when={data().dist.toolCall > 0}>
|
|
823
879
|
<text fg={pal().muted}>
|
|
824
|
-
{justify(
|
|
880
|
+
{justify(t().distTool, fmt(data().dist.toolCall), t().tok)}
|
|
825
881
|
</text>
|
|
826
882
|
</Show>
|
|
827
883
|
<Show when={data().dist.toolResult > 0}>
|
|
828
884
|
<text fg={pal().muted}>
|
|
829
|
-
{justify(
|
|
885
|
+
{justify(t().distRes, fmt(data().dist.toolResult), t().tok)}
|
|
830
886
|
</text>
|
|
831
887
|
</Show>
|
|
832
888
|
<text fg={pal().text}>
|
|
833
|
-
{justify(
|
|
889
|
+
{justify(t().distTotal, fmt(data().dist.apiInput), t().tok)}
|
|
834
890
|
</text>
|
|
835
891
|
</Show>
|
|
836
892
|
</Show>
|
|
837
893
|
</Show>
|
|
894
|
+
|
|
895
|
+
{/* ── loaded skills (collapsible, default open) ── */}
|
|
896
|
+
<Show when={sectionSkills()}>
|
|
897
|
+
<Show when={data().hasSkills}>
|
|
898
|
+
{<text onMouseUp={() => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n })}>
|
|
899
|
+
<span style={{ fg: pal().muted }}>{skillsOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
900
|
+
<span style={{ fg: pal().primary }}><b>{t().secSkills}</b></span>
|
|
901
|
+
<span style={{ fg: pal().muted }}> ({data().skills.length})</span>
|
|
902
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t().secSkills + ` (${data().skills.length})`))}</span>
|
|
903
|
+
</text>}
|
|
904
|
+
<Show when={skillsOpen()}>
|
|
905
|
+
{data().skills.map((sk: { name: string; tokens: number }) => {
|
|
906
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok)
|
|
907
|
+
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1)
|
|
908
|
+
const label = truncateVisual(sk.name, maxLabel)
|
|
909
|
+
return (
|
|
910
|
+
<text fg={pal().muted}>
|
|
911
|
+
{justify(label, fmt(sk.tokens), t().tok)}
|
|
912
|
+
</text>
|
|
913
|
+
)
|
|
914
|
+
})}
|
|
915
|
+
</Show>
|
|
916
|
+
</Show>
|
|
917
|
+
</Show>
|
|
838
918
|
</Show>
|
|
839
919
|
</Show>
|
|
840
|
-
</box>
|
|
920
|
+
</box>
|
|
841
921
|
)
|
|
842
922
|
}
|
|
843
923
|
|
|
@@ -870,14 +950,18 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
870
950
|
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
871
951
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
872
952
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
953
|
+
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
873
954
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
955
|
+
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
874
956
|
|
|
875
957
|
const signals: PanelSignals = {
|
|
876
958
|
currencySymbol, setCurrencySymbol,
|
|
877
959
|
exchangeRate, setExchangeRate,
|
|
960
|
+
langZH, setLangZH,
|
|
878
961
|
sectionDetail, setSectionDetail,
|
|
879
962
|
sectionModel, setSectionModel,
|
|
880
963
|
sectionDist, setSectionDist,
|
|
964
|
+
sectionSkills, setSectionSkills,
|
|
881
965
|
borderVisible, setBorderVisible,
|
|
882
966
|
}
|
|
883
967
|
|
|
@@ -947,6 +1031,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
947
1031
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
948
1032
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
949
1033
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1034
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
950
1035
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
951
1036
|
dialog?.replace(() => (
|
|
952
1037
|
<api.ui.DialogSelect
|
|
@@ -955,6 +1040,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
955
1040
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
956
1041
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
957
1042
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1043
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
958
1044
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
959
1045
|
]}
|
|
960
1046
|
onSelect={(opt) => {
|
|
@@ -970,6 +1056,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
970
1056
|
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
971
1057
|
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
972
1058
|
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1059
|
+
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
973
1060
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
974
1061
|
}
|
|
975
1062
|
dialog?.clear()
|
|
@@ -989,14 +1076,80 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
989
1076
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
990
1077
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
991
1078
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1079
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
992
1080
|
api.ui.toast({
|
|
993
1081
|
title: "Cache Panel Config",
|
|
994
|
-
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
|
|
1082
|
+
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
|
|
995
1083
|
duration: 8000,
|
|
996
1084
|
})
|
|
997
1085
|
dialog?.clear()
|
|
998
1086
|
},
|
|
999
1087
|
},
|
|
1088
|
+
{
|
|
1089
|
+
title: "Cache: Switch Language",
|
|
1090
|
+
value: "cache.lang",
|
|
1091
|
+
description: "Switch between Chinese and English display",
|
|
1092
|
+
slash: { name: "cache-lang" },
|
|
1093
|
+
onSelect: (dialog) => {
|
|
1094
|
+
const cur = langZH()
|
|
1095
|
+
dialog?.replace(() => (
|
|
1096
|
+
<api.ui.DialogSelect
|
|
1097
|
+
title="Display Language"
|
|
1098
|
+
options={[
|
|
1099
|
+
{ title: `中文 ${cur ? "\u2713" : ""}`, value: "zh" },
|
|
1100
|
+
{ title: `English ${cur ? "" : "\u2713"}`, value: "en" },
|
|
1101
|
+
]}
|
|
1102
|
+
onSelect={(opt) => {
|
|
1103
|
+
const zh = opt.value === "zh"
|
|
1104
|
+
api.kv.set(`${KV_PREFIX}.lang`, opt.value)
|
|
1105
|
+
setLangZH(zh)
|
|
1106
|
+
api.ui.toast({ message: zh ? "语言已切换为中文" : "Switched to English" })
|
|
1107
|
+
dialog?.clear()
|
|
1108
|
+
}}
|
|
1109
|
+
/>
|
|
1110
|
+
))
|
|
1111
|
+
},
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
title: "Cache: Debug Skills Detection",
|
|
1115
|
+
value: "cache.debug-skills",
|
|
1116
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1117
|
+
slash: { name: "cache-debug-skills" },
|
|
1118
|
+
onSelect: () => {
|
|
1119
|
+
const rt = api.route.current
|
|
1120
|
+
if (rt.name !== "session" || !rt.params) {
|
|
1121
|
+
api.ui.toast({ message: "Please run this command inside a session", variant: "warning" })
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
const sid = String(rt.params.sessionID)
|
|
1125
|
+
const msgs = api.state.session.messages(sid)
|
|
1126
|
+
const byTool: Record<string, number> = {}
|
|
1127
|
+
const skillParts: string[] = []
|
|
1128
|
+
for (const msg of msgs) {
|
|
1129
|
+
if (msg.role !== "assistant") continue
|
|
1130
|
+
let parts: readonly any[] = []
|
|
1131
|
+
try { parts = api.state.part(msg.id) } catch {}
|
|
1132
|
+
for (const p of parts) {
|
|
1133
|
+
if (p.type === "tool") {
|
|
1134
|
+
const t = String(p.tool ?? "?")
|
|
1135
|
+
byTool[t] = (byTool[t] ?? 0) + 1
|
|
1136
|
+
if (t === "skill") {
|
|
1137
|
+
const meta = p.state?.metadata
|
|
1138
|
+
const rootMeta = p.metadata
|
|
1139
|
+
skillParts.push(`state.metadata=${JSON.stringify(meta)} | root.metadata=${JSON.stringify(rootMeta)} | state.title="${p.state?.title}" | state.output[:80]="${String(p.state?.output ?? "").slice(0, 80)}"`)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ")
|
|
1145
|
+
const extra = skillParts.length > 0 ? "\n\nSkill parts:\n" + skillParts.join("\n") : "\n\n⚠ No skill tool parts found — AI may be reading SKILL.md instead. Try: 'Use the skill tool to load karpathy-guidelines'"
|
|
1146
|
+
api.ui.toast({
|
|
1147
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
1148
|
+
message: summary + extra,
|
|
1149
|
+
duration: 15000,
|
|
1150
|
+
})
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1000
1153
|
])
|
|
1001
1154
|
}
|
|
1002
1155
|
|