opencode-visual-cache 1.2.11 → 1.2.13
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 +129 -12
- package/package.json +58 -58
- package/src/_version.ts +1 -1
- package/src/index.tsx +142 -8
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.13";
|
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.13";
|
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(() => {
|
|
@@ -327,7 +348,19 @@ function TokenCachePanel(props) {
|
|
|
327
348
|
void partVersion();
|
|
328
349
|
// 自然追踪 messages 和 provider(SDK 数据就绪时自动重新执行)
|
|
329
350
|
const msgs = props.api.state.session.messages(sid);
|
|
330
|
-
|
|
351
|
+
const session = props.api.state.session.get(sid);
|
|
352
|
+
// 累计值优先使用 Session 聚合字段(数据库级,不受 sync 层 limit:100 截断)
|
|
353
|
+
// 若字段不存在(旧版本 SDK),降级到消息遍历累加
|
|
354
|
+
let input = session?.tokens?.input ?? 0;
|
|
355
|
+
let read = session?.tokens?.cache?.read ?? 0;
|
|
356
|
+
let write = session?.tokens?.cache?.write ?? 0;
|
|
357
|
+
let output = session?.tokens?.output ?? 0;
|
|
358
|
+
let cost = session?.cost ?? 0;
|
|
359
|
+
let pid = session?.model?.providerID ?? "";
|
|
360
|
+
let mid = session?.model?.id ?? "";
|
|
361
|
+
const fallbackTokens = session?.tokens == null;
|
|
362
|
+
const fallbackCost = session?.cost == null;
|
|
363
|
+
const fallbackModel = !pid || !mid;
|
|
331
364
|
let prevMsgHitRate = -1, lastMsgHitRate = -1;
|
|
332
365
|
for (const msg of msgs) {
|
|
333
366
|
if (msg.role !== "assistant")
|
|
@@ -340,12 +373,16 @@ function TokenCachePanel(props) {
|
|
|
340
373
|
prevMsgHitRate = lastMsgHitRate;
|
|
341
374
|
lastMsgHitRate = (mrt / mit) * 100;
|
|
342
375
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
376
|
+
if (fallbackTokens) {
|
|
377
|
+
input += num(t.input);
|
|
378
|
+
read += num(t.cache?.read);
|
|
379
|
+
write += num(t.cache?.write);
|
|
380
|
+
output += num(t.output);
|
|
381
|
+
}
|
|
382
|
+
if (fallbackCost) {
|
|
383
|
+
cost += num(msg.cost);
|
|
384
|
+
}
|
|
385
|
+
if (fallbackModel && msg.providerID && msg.modelID) {
|
|
349
386
|
pid = msg.providerID;
|
|
350
387
|
mid = msg.modelID;
|
|
351
388
|
}
|
|
@@ -374,8 +411,9 @@ function TokenCachePanel(props) {
|
|
|
374
411
|
const distData = untrack(() => {
|
|
375
412
|
let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
|
|
376
413
|
let hasDistData = false;
|
|
414
|
+
const loadedSkills = new Map();
|
|
377
415
|
try {
|
|
378
|
-
const
|
|
416
|
+
const cfg = props.api.state.config;
|
|
379
417
|
const agentName = String(session?.agent ?? cfg?.default_agent ?? "build");
|
|
380
418
|
const agents = cfg?.agent;
|
|
381
419
|
const agentCfg = agents?.[agentName];
|
|
@@ -431,6 +469,25 @@ function TokenCachePanel(props) {
|
|
|
431
469
|
if (e.error)
|
|
432
470
|
dist.toolResult += estimateTokens(e.error);
|
|
433
471
|
}
|
|
472
|
+
if (tp.tool === "skill" && tp.state.status === "completed") {
|
|
473
|
+
// TUI SDK strips tool metadata — extract skill name from well-known output format.
|
|
474
|
+
// Cross-validated against api.client.app.skills() when available.
|
|
475
|
+
let name = tp.state.metadata?.name;
|
|
476
|
+
if (typeof name !== "string") {
|
|
477
|
+
const m = typeof tp.state.output === "string"
|
|
478
|
+
? tp.state.output.match(/^#{1,2}\s*Skill:\s*(.+)/m)
|
|
479
|
+
: null;
|
|
480
|
+
if (m)
|
|
481
|
+
name = m[1].trim();
|
|
482
|
+
}
|
|
483
|
+
if (typeof name === "string") {
|
|
484
|
+
const tokens = typeof tp.state.output === "string" ? estimateTokens(tp.state.output) : 0;
|
|
485
|
+
const existing = loadedSkills.get(name);
|
|
486
|
+
if (!existing || existing.tokens < tokens) {
|
|
487
|
+
loadedSkills.set(name, { name, tokens });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
434
491
|
}
|
|
435
492
|
else if (p.type === "reasoning")
|
|
436
493
|
dist.agent += estimateTokens(p.text);
|
|
@@ -458,7 +515,8 @@ function TokenCachePanel(props) {
|
|
|
458
515
|
}
|
|
459
516
|
catch { }
|
|
460
517
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
461
|
-
|
|
518
|
+
const skills = [...loadedSkills.values()];
|
|
519
|
+
return { finalDist, finalHasDist, skills };
|
|
462
520
|
});
|
|
463
521
|
setDataSignal({
|
|
464
522
|
hitRate, read, write, freshInput: input, output, cost, saved, model,
|
|
@@ -466,6 +524,7 @@ function TokenCachePanel(props) {
|
|
|
466
524
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
467
525
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
468
526
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
527
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
469
528
|
});
|
|
470
529
|
});
|
|
471
530
|
const data = createMemo(() => {
|
|
@@ -505,6 +564,7 @@ function TokenCachePanel(props) {
|
|
|
505
564
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)));
|
|
506
565
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)));
|
|
507
566
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
|
|
567
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)));
|
|
508
568
|
}
|
|
509
569
|
catch { }
|
|
510
570
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -521,6 +581,7 @@ function TokenCachePanel(props) {
|
|
|
521
581
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
522
582
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
523
583
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
584
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)));
|
|
524
585
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
525
586
|
setBorderVisible(bv !== false);
|
|
526
587
|
// Restore language preference
|
|
@@ -637,7 +698,12 @@ function TokenCachePanel(props) {
|
|
|
637
698
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
638
699
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
639
700
|
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) })] })] }) })] }) })] }))
|
|
701
|
+
}, 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) => {
|
|
702
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok);
|
|
703
|
+
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
704
|
+
const label = truncateVisual(sk.name, maxLabel);
|
|
705
|
+
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t().tok) }));
|
|
706
|
+
}) })] }) })] }) })] }));
|
|
641
707
|
}
|
|
642
708
|
// ---------------------------------------------------------------------------
|
|
643
709
|
// Plugin entry
|
|
@@ -659,6 +725,7 @@ const tui = async (api) => {
|
|
|
659
725
|
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
660
726
|
const [sectionModel, setSectionModel] = createSignal(true);
|
|
661
727
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
728
|
+
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
662
729
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
663
730
|
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
664
731
|
const signals = {
|
|
@@ -668,6 +735,7 @@ const tui = async (api) => {
|
|
|
668
735
|
sectionDetail, setSectionDetail,
|
|
669
736
|
sectionModel, setSectionModel,
|
|
670
737
|
sectionDist, setSectionDist,
|
|
738
|
+
sectionSkills, setSectionSkills,
|
|
671
739
|
borderVisible, setBorderVisible,
|
|
672
740
|
};
|
|
673
741
|
api.slots.register(createSidebarSlot(api, signals));
|
|
@@ -721,11 +789,13 @@ const tui = async (api) => {
|
|
|
721
789
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
722
790
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
723
791
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
792
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
724
793
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
725
794
|
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
|
|
726
795
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
727
796
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
728
797
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
798
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
729
799
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
730
800
|
], onSelect: (opt) => {
|
|
731
801
|
if (opt.value === "border") {
|
|
@@ -744,6 +814,8 @@ const tui = async (api) => {
|
|
|
744
814
|
signals.setSectionModel(!cur);
|
|
745
815
|
if (opt.value === "dist")
|
|
746
816
|
signals.setSectionDist(!cur);
|
|
817
|
+
if (opt.value === "skills")
|
|
818
|
+
signals.setSectionSkills(!cur);
|
|
747
819
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
748
820
|
}
|
|
749
821
|
dialog?.clear();
|
|
@@ -761,9 +833,10 @@ const tui = async (api) => {
|
|
|
761
833
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
762
834
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
763
835
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
836
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
764
837
|
api.ui.toast({
|
|
765
838
|
title: "Cache Panel Config",
|
|
766
|
-
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
|
|
839
|
+
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
|
|
767
840
|
duration: 8000,
|
|
768
841
|
});
|
|
769
842
|
dialog?.clear();
|
|
@@ -788,6 +861,50 @@ const tui = async (api) => {
|
|
|
788
861
|
} })));
|
|
789
862
|
},
|
|
790
863
|
},
|
|
864
|
+
{
|
|
865
|
+
title: "Cache: Debug Skills Detection",
|
|
866
|
+
value: "cache.debug-skills",
|
|
867
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
868
|
+
slash: { name: "cache-debug-skills" },
|
|
869
|
+
onSelect: () => {
|
|
870
|
+
const rt = api.route.current;
|
|
871
|
+
if (rt.name !== "session" || !rt.params) {
|
|
872
|
+
api.ui.toast({ message: "Please run this command inside a session", variant: "warning" });
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const sid = String(rt.params.sessionID);
|
|
876
|
+
const msgs = api.state.session.messages(sid);
|
|
877
|
+
const byTool = {};
|
|
878
|
+
const skillParts = [];
|
|
879
|
+
for (const msg of msgs) {
|
|
880
|
+
if (msg.role !== "assistant")
|
|
881
|
+
continue;
|
|
882
|
+
let parts = [];
|
|
883
|
+
try {
|
|
884
|
+
parts = api.state.part(msg.id);
|
|
885
|
+
}
|
|
886
|
+
catch { }
|
|
887
|
+
for (const p of parts) {
|
|
888
|
+
if (p.type === "tool") {
|
|
889
|
+
const t = String(p.tool ?? "?");
|
|
890
|
+
byTool[t] = (byTool[t] ?? 0) + 1;
|
|
891
|
+
if (t === "skill") {
|
|
892
|
+
const meta = p.state?.metadata;
|
|
893
|
+
const rootMeta = p.metadata;
|
|
894
|
+
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)}"`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ");
|
|
900
|
+
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'";
|
|
901
|
+
api.ui.toast({
|
|
902
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
903
|
+
message: summary + extra,
|
|
904
|
+
duration: 15000,
|
|
905
|
+
});
|
|
906
|
+
},
|
|
907
|
+
},
|
|
791
908
|
]);
|
|
792
909
|
};
|
|
793
910
|
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.13",
|
|
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.13";
|
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
|
|
|
@@ -411,16 +431,37 @@ function TokenCachePanel(props: {
|
|
|
411
431
|
|
|
412
432
|
// 自然追踪 messages 和 provider(SDK 数据就绪时自动重新执行)
|
|
413
433
|
const msgs = props.api.state.session.messages(sid) as Message[]
|
|
414
|
-
|
|
434
|
+
const session = props.api.state.session.get(sid)
|
|
435
|
+
|
|
436
|
+
// 累计值优先使用 Session 聚合字段(数据库级,不受 sync 层 limit:100 截断)
|
|
437
|
+
// 若字段不存在(旧版本 SDK),降级到消息遍历累加
|
|
438
|
+
let input = session?.tokens?.input ?? 0
|
|
439
|
+
let read = session?.tokens?.cache?.read ?? 0
|
|
440
|
+
let write = session?.tokens?.cache?.write ?? 0
|
|
441
|
+
let output = session?.tokens?.output ?? 0
|
|
442
|
+
let cost = session?.cost ?? 0
|
|
443
|
+
let pid = session?.model?.providerID ?? ""
|
|
444
|
+
let mid = session?.model?.id ?? ""
|
|
445
|
+
|
|
446
|
+
const fallbackTokens = session?.tokens == null
|
|
447
|
+
const fallbackCost = session?.cost == null
|
|
448
|
+
const fallbackModel = !pid || !mid
|
|
449
|
+
|
|
415
450
|
let prevMsgHitRate = -1, lastMsgHitRate = -1
|
|
416
451
|
for (const msg of msgs) {
|
|
417
452
|
if (msg.role !== "assistant") continue
|
|
418
453
|
const t = (msg as AssistantMessage).tokens; if (!t) continue
|
|
419
454
|
const mit = num(t.input) + num(t.cache?.read), mrt = num(t.cache?.read)
|
|
420
455
|
if (mit > 0) { prevMsgHitRate = lastMsgHitRate; lastMsgHitRate = (mrt / mit) * 100 }
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
456
|
+
if (fallbackTokens) {
|
|
457
|
+
input += num(t.input); read += num(t.cache?.read); write += num(t.cache?.write); output += num(t.output)
|
|
458
|
+
}
|
|
459
|
+
if (fallbackCost) {
|
|
460
|
+
cost += num((msg as AssistantMessage).cost)
|
|
461
|
+
}
|
|
462
|
+
if (fallbackModel && (msg as AssistantMessage).providerID && (msg as AssistantMessage).modelID) {
|
|
463
|
+
pid = (msg as AssistantMessage).providerID; mid = (msg as AssistantMessage).modelID
|
|
464
|
+
}
|
|
424
465
|
}
|
|
425
466
|
let saved = 0, inputRate = 0, cacheReadRate = 0, cacheWriteRate = 0
|
|
426
467
|
if (read > 0 && pid && mid) for (const provider of props.api.state.provider) {
|
|
@@ -440,8 +481,9 @@ function TokenCachePanel(props: {
|
|
|
440
481
|
const distData = untrack(() => {
|
|
441
482
|
let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 }
|
|
442
483
|
let hasDistData = false
|
|
484
|
+
const loadedSkills = new Map<string, { name: string; tokens: number }>()
|
|
443
485
|
try {
|
|
444
|
-
const
|
|
486
|
+
const cfg = props.api.state.config as Record<string, unknown>
|
|
445
487
|
const agentName = String(session?.agent ?? (cfg as any)?.default_agent ?? "build")
|
|
446
488
|
const agents = cfg?.agent as Record<string, unknown> | undefined
|
|
447
489
|
const agentCfg = agents?.[agentName] as Record<string, unknown> | undefined
|
|
@@ -467,6 +509,24 @@ function TokenCachePanel(props: {
|
|
|
467
509
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
468
510
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
469
511
|
else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
|
|
512
|
+
if (tp.tool === "skill" && tp.state.status === "completed") {
|
|
513
|
+
// TUI SDK strips tool metadata — extract skill name from well-known output format.
|
|
514
|
+
// Cross-validated against api.client.app.skills() when available.
|
|
515
|
+
let name: string | undefined = tp.state.metadata?.name
|
|
516
|
+
if (typeof name !== "string") {
|
|
517
|
+
const m = typeof tp.state.output === "string"
|
|
518
|
+
? tp.state.output.match(/^#{1,2}\s*Skill:\s*(.+)/m)
|
|
519
|
+
: null
|
|
520
|
+
if (m) name = m[1].trim()
|
|
521
|
+
}
|
|
522
|
+
if (typeof name === "string") {
|
|
523
|
+
const tokens = typeof tp.state.output === "string" ? estimateTokens(tp.state.output) : 0
|
|
524
|
+
const existing = loadedSkills.get(name)
|
|
525
|
+
if (!existing || existing.tokens < tokens) {
|
|
526
|
+
loadedSkills.set(name, { name, tokens })
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
470
530
|
} else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
|
|
471
531
|
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
472
532
|
}
|
|
@@ -484,7 +544,8 @@ function TokenCachePanel(props: {
|
|
|
484
544
|
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
|
|
485
545
|
} catch {}
|
|
486
546
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
487
|
-
|
|
547
|
+
const skills = [...loadedSkills.values()]
|
|
548
|
+
return { finalDist, finalHasDist, skills }
|
|
488
549
|
})
|
|
489
550
|
|
|
490
551
|
setDataSignal({
|
|
@@ -493,6 +554,7 @@ function TokenCachePanel(props: {
|
|
|
493
554
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
494
555
|
trend, hasTrendData, providerName, sessionHitRate,
|
|
495
556
|
dist: distData.finalDist, hasDistData: distData.finalHasDist,
|
|
557
|
+
skills: distData.skills, hasSkills: distData.skills.length > 0,
|
|
496
558
|
})
|
|
497
559
|
})
|
|
498
560
|
|
|
@@ -532,6 +594,7 @@ function TokenCachePanel(props: {
|
|
|
532
594
|
setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)))
|
|
533
595
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)))
|
|
534
596
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
|
|
597
|
+
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)))
|
|
535
598
|
} catch {}
|
|
536
599
|
|
|
537
600
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -546,6 +609,7 @@ function TokenCachePanel(props: {
|
|
|
546
609
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
547
610
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
548
611
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
612
|
+
setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)))
|
|
549
613
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
550
614
|
setBorderVisible(bv !== false)
|
|
551
615
|
// Restore language preference
|
|
@@ -848,9 +912,33 @@ function TokenCachePanel(props: {
|
|
|
848
912
|
</Show>
|
|
849
913
|
</Show>
|
|
850
914
|
</Show>
|
|
915
|
+
|
|
916
|
+
{/* ── loaded skills (collapsible, default open) ── */}
|
|
917
|
+
<Show when={sectionSkills()}>
|
|
918
|
+
<Show when={data().hasSkills}>
|
|
919
|
+
{<text onMouseUp={() => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n })}>
|
|
920
|
+
<span style={{ fg: pal().muted }}>{skillsOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
921
|
+
<span style={{ fg: pal().primary }}><b>{t().secSkills}</b></span>
|
|
922
|
+
<span style={{ fg: pal().muted }}> ({data().skills.length})</span>
|
|
923
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t().secSkills + ` (${data().skills.length})`))}</span>
|
|
924
|
+
</text>}
|
|
925
|
+
<Show when={skillsOpen()}>
|
|
926
|
+
{data().skills.map((sk: { name: string; tokens: number }) => {
|
|
927
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok)
|
|
928
|
+
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1)
|
|
929
|
+
const label = truncateVisual(sk.name, maxLabel)
|
|
930
|
+
return (
|
|
931
|
+
<text fg={pal().muted}>
|
|
932
|
+
{justify(label, fmt(sk.tokens), t().tok)}
|
|
933
|
+
</text>
|
|
934
|
+
)
|
|
935
|
+
})}
|
|
936
|
+
</Show>
|
|
937
|
+
</Show>
|
|
938
|
+
</Show>
|
|
851
939
|
</Show>
|
|
852
940
|
</Show>
|
|
853
|
-
</box>
|
|
941
|
+
</box>
|
|
854
942
|
)
|
|
855
943
|
}
|
|
856
944
|
|
|
@@ -883,6 +971,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
883
971
|
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
884
972
|
const [sectionModel, setSectionModel] = createSignal(true)
|
|
885
973
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
974
|
+
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
886
975
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
887
976
|
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
888
977
|
|
|
@@ -893,6 +982,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
893
982
|
sectionDetail, setSectionDetail,
|
|
894
983
|
sectionModel, setSectionModel,
|
|
895
984
|
sectionDist, setSectionDist,
|
|
985
|
+
sectionSkills, setSectionSkills,
|
|
896
986
|
borderVisible, setBorderVisible,
|
|
897
987
|
}
|
|
898
988
|
|
|
@@ -962,6 +1052,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
962
1052
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
963
1053
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
964
1054
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1055
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
965
1056
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
966
1057
|
dialog?.replace(() => (
|
|
967
1058
|
<api.ui.DialogSelect
|
|
@@ -970,6 +1061,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
970
1061
|
{ title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
|
|
971
1062
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
972
1063
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1064
|
+
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
973
1065
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
974
1066
|
]}
|
|
975
1067
|
onSelect={(opt) => {
|
|
@@ -985,6 +1077,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
985
1077
|
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
986
1078
|
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
987
1079
|
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1080
|
+
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
988
1081
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
989
1082
|
}
|
|
990
1083
|
dialog?.clear()
|
|
@@ -1004,9 +1097,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1004
1097
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
1005
1098
|
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1006
1099
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1100
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1007
1101
|
api.ui.toast({
|
|
1008
1102
|
title: "Cache Panel Config",
|
|
1009
|
-
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"}`,
|
|
1103
|
+
message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
|
|
1010
1104
|
duration: 8000,
|
|
1011
1105
|
})
|
|
1012
1106
|
dialog?.clear()
|
|
@@ -1037,6 +1131,46 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1037
1131
|
))
|
|
1038
1132
|
},
|
|
1039
1133
|
},
|
|
1134
|
+
{
|
|
1135
|
+
title: "Cache: Debug Skills Detection",
|
|
1136
|
+
value: "cache.debug-skills",
|
|
1137
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1138
|
+
slash: { name: "cache-debug-skills" },
|
|
1139
|
+
onSelect: () => {
|
|
1140
|
+
const rt = api.route.current
|
|
1141
|
+
if (rt.name !== "session" || !rt.params) {
|
|
1142
|
+
api.ui.toast({ message: "Please run this command inside a session", variant: "warning" })
|
|
1143
|
+
return
|
|
1144
|
+
}
|
|
1145
|
+
const sid = String(rt.params.sessionID)
|
|
1146
|
+
const msgs = api.state.session.messages(sid)
|
|
1147
|
+
const byTool: Record<string, number> = {}
|
|
1148
|
+
const skillParts: string[] = []
|
|
1149
|
+
for (const msg of msgs) {
|
|
1150
|
+
if (msg.role !== "assistant") continue
|
|
1151
|
+
let parts: readonly any[] = []
|
|
1152
|
+
try { parts = api.state.part(msg.id) } catch {}
|
|
1153
|
+
for (const p of parts) {
|
|
1154
|
+
if (p.type === "tool") {
|
|
1155
|
+
const t = String(p.tool ?? "?")
|
|
1156
|
+
byTool[t] = (byTool[t] ?? 0) + 1
|
|
1157
|
+
if (t === "skill") {
|
|
1158
|
+
const meta = p.state?.metadata
|
|
1159
|
+
const rootMeta = p.metadata
|
|
1160
|
+
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)}"`)
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ")
|
|
1166
|
+
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'"
|
|
1167
|
+
api.ui.toast({
|
|
1168
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
1169
|
+
message: summary + extra,
|
|
1170
|
+
duration: 15000,
|
|
1171
|
+
})
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1040
1174
|
])
|
|
1041
1175
|
}
|
|
1042
1176
|
|