opencode-visual-cache 1.2.11 → 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 +4 -2
- package/README_EN.md +3 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +105 -4
- package/package.json +58 -58
- package/src/_version.ts +1 -1
- package/src/index.tsx +116 -3
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
- **语言适配**:自动检测系统语言,支持 `/cache-lang` 运行时切换中/英文,偏好持久化
|
|
48
48
|
- **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
|
|
49
49
|
- **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板
|
|
50
|
+
- **已加载技能**:检测 session 中 LLM 调用 `skill` tool 的记录,展示已加载技能名及估算 Token 占用
|
|
50
51
|
|
|
51
52
|
---
|
|
52
53
|
|
|
@@ -97,7 +98,7 @@ 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` | 查看当前配置 | 弹出当前货币、汇率、区块可见性状态 |
|
|
102
103
|
| `/cache-lang` | 切换显示语言 | 从列表选择中文或 English,界面即时切换,无需重启 |
|
|
103
104
|
|
|
@@ -127,11 +128,12 @@ npm install -g opencode-visual-cache@latest
|
|
|
127
128
|
|
|
128
129
|
### 4.3 区块可见性
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
面板中的子区块可以独立关闭,方便在侧边栏空间紧张时隐藏不需要的信息:
|
|
131
132
|
|
|
132
133
|
- **Token 明细**:缓存读 / 缓存写 / 未命中 / 输出
|
|
133
134
|
- **模型与定价**:费用 / 提供商 / 模型名 / 单价
|
|
134
135
|
- **估算 Token 分布**:按角色拆分的 Token 估算
|
|
136
|
+
- **已加载技能**:session 中 LLM 实际调用过的 Skill 名及估算 Token 占用
|
|
135
137
|
|
|
136
138
|
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
137
139
|
|
package/README_EN.md
CHANGED
|
@@ -47,6 +47,7 @@ If you find this plugin useful, a ⭐ would mean a lot — thank you!<br>
|
|
|
47
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
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,7 +98,7 @@ 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 |
|
|
102
103
|
| `/cache-lang` | Switch display language | Pick Chinese or English from the dialog — takes effect immediately, no restart needed |
|
|
103
104
|
|
|
@@ -132,6 +133,7 @@ Three sub-sections can be toggled independently to save sidebar space:
|
|
|
132
133
|
- **Token Detail**: cache read / write / miss / output
|
|
133
134
|
- **Model & Pricing**: cost / provider / model name / per-million rates
|
|
134
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
|
|
135
137
|
|
|
136
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.
|
|
137
139
|
|
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 ──────────────────────────────────────────────────────
|
|
@@ -77,6 +93,7 @@ const ZH_T = {
|
|
|
77
93
|
distOut: "输出:",
|
|
78
94
|
secDetail: "明细",
|
|
79
95
|
secModel: "模型",
|
|
96
|
+
secSkills: "已加载技能",
|
|
80
97
|
};
|
|
81
98
|
const EN_T = {
|
|
82
99
|
title: "Token Cache",
|
|
@@ -107,6 +124,7 @@ const EN_T = {
|
|
|
107
124
|
distOut: "Output:",
|
|
108
125
|
secDetail: "Detail",
|
|
109
126
|
secModel: "Model",
|
|
127
|
+
secSkills: "Loaded Skills",
|
|
110
128
|
};
|
|
111
129
|
// ── color helpers ────────────────────────────────────────────────
|
|
112
130
|
/** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
|
|
@@ -294,9 +312,10 @@ function TokenCachePanel(props) {
|
|
|
294
312
|
const [detailOpen, setDetailOpen] = createSignal(true);
|
|
295
313
|
const [modelOpen, setModelOpen] = createSignal(true);
|
|
296
314
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
315
|
+
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
297
316
|
let boxEl;
|
|
298
317
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
299
|
-
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, 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;
|
|
300
319
|
// ── reactive translation (follows langZH signal) ──
|
|
301
320
|
const t = createMemo(() => langZH() ? ZH_T : EN_T);
|
|
302
321
|
// ── scan session messages reactively ──
|
|
@@ -319,6 +338,8 @@ function TokenCachePanel(props) {
|
|
|
319
338
|
providerName: "", sessionHitRate: 0,
|
|
320
339
|
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
321
340
|
hasDistData: false,
|
|
341
|
+
skills: [],
|
|
342
|
+
hasSkills: false,
|
|
322
343
|
});
|
|
323
344
|
const [refreshTick, setRefreshTick] = createSignal(0);
|
|
324
345
|
createEffect(() => {
|
|
@@ -374,6 +395,7 @@ function TokenCachePanel(props) {
|
|
|
374
395
|
const distData = untrack(() => {
|
|
375
396
|
let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
|
|
376
397
|
let hasDistData = false;
|
|
398
|
+
const loadedSkills = new Map();
|
|
377
399
|
try {
|
|
378
400
|
const session = props.api.state.session.get(sid), cfg = props.api.state.config;
|
|
379
401
|
const agentName = String(session?.agent ?? cfg?.default_agent ?? "build");
|
|
@@ -431,6 +453,25 @@ function TokenCachePanel(props) {
|
|
|
431
453
|
if (e.error)
|
|
432
454
|
dist.toolResult += estimateTokens(e.error);
|
|
433
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
|
+
}
|
|
434
475
|
}
|
|
435
476
|
else if (p.type === "reasoning")
|
|
436
477
|
dist.agent += estimateTokens(p.text);
|
|
@@ -458,7 +499,8 @@ function TokenCachePanel(props) {
|
|
|
458
499
|
}
|
|
459
500
|
catch { }
|
|
460
501
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
461
|
-
|
|
502
|
+
const skills = [...loadedSkills.values()];
|
|
503
|
+
return { finalDist, finalHasDist, skills };
|
|
462
504
|
});
|
|
463
505
|
setDataSignal({
|
|
464
506
|
hitRate, read, write, freshInput: input, output, cost, saved, model,
|
|
@@ -466,6 +508,7 @@ function TokenCachePanel(props) {
|
|
|
466
508
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
467
509
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
468
510
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
511
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
469
512
|
});
|
|
470
513
|
});
|
|
471
514
|
const data = createMemo(() => {
|
|
@@ -505,6 +548,7 @@ function TokenCachePanel(props) {
|
|
|
505
548
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)));
|
|
506
549
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)));
|
|
507
550
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
|
|
551
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)));
|
|
508
552
|
}
|
|
509
553
|
catch { }
|
|
510
554
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -521,6 +565,7 @@ function TokenCachePanel(props) {
|
|
|
521
565
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
522
566
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
523
567
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
568
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)));
|
|
524
569
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
525
570
|
setBorderVisible(bv !== false);
|
|
526
571
|
// Restore language preference
|
|
@@ -637,7 +682,12 @@ function TokenCachePanel(props) {
|
|
|
637
682
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
638
683
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
639
684
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
640
|
-
}, 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) })] })] }) })] }) })] }))
|
|
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
|
+
}) })] }) })] }) })] }));
|
|
641
691
|
}
|
|
642
692
|
// ---------------------------------------------------------------------------
|
|
643
693
|
// Plugin entry
|
|
@@ -659,6 +709,7 @@ const tui = async (api) => {
|
|
|
659
709
|
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
660
710
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
661
711
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
712
|
+
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
662
713
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
663
714
|
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
664
715
|
const signals = {
|
|
@@ -668,6 +719,7 @@ const tui = async (api) => {
|
|
|
668
719
|
sectionDetail, setSectionDetail,
|
|
669
720
|
sectionModel, setSectionModel,
|
|
670
721
|
sectionDist, setSectionDist,
|
|
722
|
+
sectionSkills, setSectionSkills,
|
|
671
723
|
borderVisible, setBorderVisible,
|
|
672
724
|
};
|
|
673
725
|
api.slots.register(createSidebarSlot(api, signals));
|
|
@@ -721,11 +773,13 @@ const tui = async (api) => {
|
|
|
721
773
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
722
774
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
723
775
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
776
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
724
777
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
725
778
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
|
|
726
779
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
727
780
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
728
781
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
782
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
729
783
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
730
784
|
], onSelect: (opt) => {
|
|
731
785
|
if (opt.value === "border") {
|
|
@@ -744,6 +798,8 @@ const tui = async (api) => {
|
|
|
744
798
|
signals.setSectionModel(!cur);
|
|
745
799
|
if (opt.value === "dist")
|
|
746
800
|
signals.setSectionDist(!cur);
|
|
801
|
+
if (opt.value === "skills")
|
|
802
|
+
signals.setSectionSkills(!cur);
|
|
747
803
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
748
804
|
}
|
|
749
805
|
dialog?.clear();
|
|
@@ -761,9 +817,10 @@ const tui = async (api) => {
|
|
|
761
817
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
762
818
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
763
819
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
820
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
764
821
|
api.ui.toast({
|
|
765
822
|
title: "Cache Panel Config",
|
|
766
|
-
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"}`,
|
|
767
824
|
duration: 8000,
|
|
768
825
|
});
|
|
769
826
|
dialog?.clear();
|
|
@@ -788,6 +845,50 @@ const tui = async (api) => {
|
|
|
788
845
|
} })));
|
|
789
846
|
},
|
|
790
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
|
+
},
|
|
791
892
|
]);
|
|
792
893
|
};
|
|
793
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
|
|
|
@@ -100,6 +112,7 @@ const ZH_T = {
|
|
|
100
112
|
distOut: "输出:",
|
|
101
113
|
secDetail: "明细",
|
|
102
114
|
secModel: "模型",
|
|
115
|
+
secSkills: "已加载技能",
|
|
103
116
|
} as const
|
|
104
117
|
|
|
105
118
|
const EN_T = {
|
|
@@ -131,6 +144,7 @@ const EN_T = {
|
|
|
131
144
|
distOut: "Output:",
|
|
132
145
|
secDetail: "Detail",
|
|
133
146
|
secModel: "Model",
|
|
147
|
+
secSkills: "Loaded Skills",
|
|
134
148
|
} as const
|
|
135
149
|
|
|
136
150
|
// ── color helpers ────────────────────────────────────────────────
|
|
@@ -327,6 +341,8 @@ interface PanelSignals {
|
|
|
327
341
|
setSectionModel: (v: boolean) => void
|
|
328
342
|
sectionDist: () => boolean
|
|
329
343
|
setSectionDist: (v: boolean) => void
|
|
344
|
+
sectionSkills: () => boolean
|
|
345
|
+
setSectionSkills: (v: boolean) => void
|
|
330
346
|
borderVisible: () => boolean
|
|
331
347
|
setBorderVisible: (v: boolean) => void
|
|
332
348
|
}
|
|
@@ -363,6 +379,7 @@ function TokenCachePanel(props: {
|
|
|
363
379
|
const [detailOpen, setDetailOpen] = createSignal(true)
|
|
364
380
|
const [modelOpen, setModelOpen] = createSignal(true)
|
|
365
381
|
const [distOpen, setDistOpen] = createSignal(false)
|
|
382
|
+
const [skillsOpen, setSkillsOpen] = createSignal(true)
|
|
366
383
|
let boxEl: any
|
|
367
384
|
|
|
368
385
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
@@ -373,6 +390,7 @@ function TokenCachePanel(props: {
|
|
|
373
390
|
sectionDetail, setSectionDetail,
|
|
374
391
|
sectionModel, setSectionModel,
|
|
375
392
|
sectionDist, setSectionDist,
|
|
393
|
+
sectionSkills, setSectionSkills,
|
|
376
394
|
borderVisible, setBorderVisible,
|
|
377
395
|
} = props.signals
|
|
378
396
|
|
|
@@ -401,6 +419,8 @@ function TokenCachePanel(props: {
|
|
|
401
419
|
providerName: "", sessionHitRate: 0,
|
|
402
420
|
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
403
421
|
hasDistData: false,
|
|
422
|
+
skills: [] as { name: string; tokens: number }[],
|
|
423
|
+
hasSkills: false,
|
|
404
424
|
})
|
|
405
425
|
const [refreshTick, setRefreshTick] = createSignal(0)
|
|
406
426
|
|
|
@@ -440,6 +460,7 @@ function TokenCachePanel(props: {
|
|
|
440
460
|
const distData = untrack(() => {
|
|
441
461
|
let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 }
|
|
442
462
|
let hasDistData = false
|
|
463
|
+
const loadedSkills = new Map<string, { name: string; tokens: number }>()
|
|
443
464
|
try {
|
|
444
465
|
const session = props.api.state.session.get(sid), cfg = props.api.state.config as Record<string, unknown>
|
|
445
466
|
const agentName = String(session?.agent ?? (cfg as any)?.default_agent ?? "build")
|
|
@@ -467,6 +488,24 @@ function TokenCachePanel(props: {
|
|
|
467
488
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
468
489
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
469
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
|
+
}
|
|
470
509
|
} else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
|
|
471
510
|
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
472
511
|
}
|
|
@@ -484,7 +523,8 @@ function TokenCachePanel(props: {
|
|
|
484
523
|
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
|
|
485
524
|
} catch {}
|
|
486
525
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
487
|
-
|
|
526
|
+
const skills = [...loadedSkills.values()]
|
|
527
|
+
return { finalDist, finalHasDist, skills }
|
|
488
528
|
})
|
|
489
529
|
|
|
490
530
|
setDataSignal({
|
|
@@ -493,6 +533,7 @@ function TokenCachePanel(props: {
|
|
|
493
533
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
494
534
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
495
535
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
536
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
496
537
|
})
|
|
497
538
|
})
|
|
498
539
|
|
|
@@ -532,6 +573,7 @@ function TokenCachePanel(props: {
|
|
|
532
573
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)))
|
|
533
574
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)))
|
|
534
575
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
|
|
576
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)))
|
|
535
577
|
} catch {}
|
|
536
578
|
|
|
537
579
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -546,6 +588,7 @@ function TokenCachePanel(props: {
|
|
|
546
588
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
547
589
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
548
590
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
591
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)))
|
|
549
592
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
550
593
|
setBorderVisible(bv !== false)
|
|
551
594
|
// Restore language preference
|
|
@@ -848,9 +891,33 @@ function TokenCachePanel(props: {
|
|
|
848
891
|
</Show>
|
|
849
892
|
</Show>
|
|
850
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>
|
|
851
918
|
</Show>
|
|
852
919
|
</Show>
|
|
853
|
-
</box>
|
|
920
|
+
</box>
|
|
854
921
|
)
|
|
855
922
|
}
|
|
856
923
|
|
|
@@ -883,6 +950,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
883
950
|
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
884
951
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
885
952
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
953
|
+
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
886
954
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
887
955
|
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
888
956
|
|
|
@@ -893,6 +961,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
893
961
|
sectionDetail, setSectionDetail,
|
|
894
962
|
sectionModel, setSectionModel,
|
|
895
963
|
sectionDist, setSectionDist,
|
|
964
|
+
sectionSkills, setSectionSkills,
|
|
896
965
|
borderVisible, setBorderVisible,
|
|
897
966
|
}
|
|
898
967
|
|
|
@@ -962,6 +1031,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
962
1031
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
963
1032
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
964
1033
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1034
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
965
1035
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
966
1036
|
dialog?.replace(() => (
|
|
967
1037
|
<api.ui.DialogSelect
|
|
@@ -970,6 +1040,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
970
1040
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
971
1041
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
972
1042
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1043
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
973
1044
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
974
1045
|
]}
|
|
975
1046
|
onSelect={(opt) => {
|
|
@@ -985,6 +1056,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
985
1056
|
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
986
1057
|
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
987
1058
|
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1059
|
+
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
988
1060
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
989
1061
|
}
|
|
990
1062
|
dialog?.clear()
|
|
@@ -1004,9 +1076,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1004
1076
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
1005
1077
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1006
1078
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1079
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1007
1080
|
api.ui.toast({
|
|
1008
1081
|
title: "Cache Panel Config",
|
|
1009
|
-
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"}`,
|
|
1010
1083
|
duration: 8000,
|
|
1011
1084
|
})
|
|
1012
1085
|
dialog?.clear()
|
|
@@ -1037,6 +1110,46 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1037
1110
|
))
|
|
1038
1111
|
},
|
|
1039
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
|
+
},
|
|
1040
1153
|
])
|
|
1041
1154
|
}
|
|
1042
1155
|
|