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 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
 
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.11";
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.11";
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
- return { finalDist, finalHasDist };
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.11",
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.11";
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
- return { finalDist, finalHasDist }
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