opencode-visual-cache 1.2.9-beta.0 → 1.2.10

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
@@ -134,6 +134,8 @@ npm install -g opencode-visual-cache@latest
134
134
 
135
135
  通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
136
136
 
137
+ > **关于 Token 分布数值**:分布面板中"总计"为最后一次 API 调用的精确 token 数,"系统提示"/"用户"等分项为字符级 BPE 估算值。分项之和通常小于总计,差值主要来自 OpenCode 运行时注入的系统提示组成部分,包括环境信息、Skill 目录、工具 Schema 定义等(详见 [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts)、[`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts))。这些内容不在 agent 配置的 `prompt` 字段中,因此插件无法估算,属于预期行为。
138
+
137
139
  ---
138
140
 
139
141
  ## 5. 更新
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.9-beta.0";
1
+ export declare const PLUGIN_VERSION = "1.2.10";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.2.9-beta.0";
2
+ export const PLUGIN_VERSION = "1.2.10";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
2
- import { createMemo, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
2
+ import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js";
3
3
  import { PLUGIN_VERSION } from "./_version";
4
4
  // ── terminal-width helpers ────────────────────────────────────────
5
5
  // CJK characters occupy 2 terminal columns; padEnd/padStart count
@@ -216,7 +216,7 @@ function fmt(n) {
216
216
  if (n >= 1_000_000)
217
217
  return (n / 1_000_000).toFixed(1) + "M";
218
218
  if (n >= 10_000)
219
- return Math.round(n / 1_000) + "K";
219
+ return (n / 1_000).toFixed(1) + "K";
220
220
  return n.toLocaleString("en-US");
221
221
  }
222
222
  function num(v) {
@@ -256,16 +256,19 @@ function estimateTokens(text) {
256
256
  else
257
257
  ascii++;
258
258
  }
259
- // Tighten the ASCII ratio for structured content where punctuation is
260
- // token-dense. JSON key-value patterns and code keywords are strong
261
- // signals that the default 4:1 ratio will materially under-estimate.
259
+ // Real BPE tokenizers (cl100k_base, o200k_base) average ~3.5-4.0
260
+ // ASCII chars/token for both JSON and source code close to prose.
261
+ // The old 2.0 / 2.5 ratios matched minified-JS extremes, not typical
262
+ // payloads, and systematically over-estimated token counts.
262
263
  const trimmed = text.trimStart();
263
- const jsonLike = (trimmed.startsWith("{") || trimmed.startsWith("["))
264
+ // Strip markdown code-fence prefix so that ```json … is detected as JSON
265
+ const strippedFence = trimmed.replace(/^\x60{3}\w*\s*\n?/, "");
266
+ const jsonLike = (strippedFence.startsWith("{") || strippedFence.startsWith("["))
264
267
  && /"[^"]+"\s*:/.test(text);
265
268
  const codeLike = !jsonLike
266
269
  && /```|^import |^export |^function |^const |^let |^var |^class |^interface |^type |^def |^fn |^pub |^use |^mod |^package /m.test(text);
267
- const asciiPerToken = jsonLike ? 2 : codeLike ? 2.5 : 4;
268
- return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.5));
270
+ const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4;
271
+ return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
269
272
  }
270
273
  const CURRENCIES = {
271
274
  USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
@@ -306,29 +309,33 @@ function TokenCachePanel(props) {
306
309
  output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
307
310
  });
308
311
  const [lastHasDist, setLastHasDist] = createSignal(false);
309
- const data = createMemo(() => {
310
- const msgs = props.api.state.session.messages(props.sessionId);
311
- let input = 0;
312
- let read = 0;
313
- let write = 0;
314
- let output = 0;
315
- let cost = 0;
316
- let pid = "";
317
- let mid = "";
318
- // Track individual hit rates per assistant message to compute trend
319
- let prevMsgHitRate = -1;
320
- let lastMsgHitRate = -1;
312
+ const [dataSignal, setDataSignal] = createSignal({
313
+ hitRate: 0, read: 0, write: 0, freshInput: 0, output: 0,
314
+ cost: 0, saved: 0, model: "", inputRate: 0, cacheReadRate: 0, cacheWriteRate: 0,
315
+ hasPricing: false, hasData: false, trend: 0, hasTrendData: false,
316
+ providerName: "", sessionHitRate: 0,
317
+ dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
318
+ hasDistData: false,
319
+ });
320
+ const [refreshTick, setRefreshTick] = createSignal(0);
321
+ createEffect(() => {
322
+ const sid = props.sessionId;
323
+ void refreshTick();
324
+ void partVersion();
325
+ // 自然追踪 messages 和 provider(SDK 数据就绪时自动重新执行)
326
+ const msgs = props.api.state.session.messages(sid);
327
+ let input = 0, read = 0, write = 0, output = 0, cost = 0, pid = "", mid = "";
328
+ let prevMsgHitRate = -1, lastMsgHitRate = -1;
321
329
  for (const msg of msgs) {
322
330
  if (msg.role !== "assistant")
323
331
  continue;
324
332
  const t = msg.tokens;
325
333
  if (!t)
326
334
  continue;
327
- const msgInputTokens = num(t.input) + num(t.cache?.read);
328
- const msgReadTokens = num(t.cache?.read);
329
- if (msgInputTokens > 0) {
335
+ const mit = num(t.input) + num(t.cache?.read), mrt = num(t.cache?.read);
336
+ if (mit > 0) {
330
337
  prevMsgHitRate = lastMsgHitRate;
331
- lastMsgHitRate = (msgReadTokens / msgInputTokens) * 100;
338
+ lastMsgHitRate = (mrt / mit) * 100;
332
339
  }
333
340
  input += num(t.input);
334
341
  read += num(t.cache?.read);
@@ -340,12 +347,8 @@ function TokenCachePanel(props) {
340
347
  mid = msg.modelID;
341
348
  }
342
349
  }
343
- // cost savings from cache hits
344
- let saved = 0;
345
- let inputRate = 0;
346
- let cacheReadRate = 0;
347
- let cacheWriteRate = 0;
348
- if (read > 0 && pid && mid) {
350
+ let saved = 0, inputRate = 0, cacheReadRate = 0, cacheWriteRate = 0;
351
+ if (read > 0 && pid && mid)
349
352
  for (const provider of props.api.state.provider) {
350
353
  if (provider.id !== pid)
351
354
  continue;
@@ -355,152 +358,115 @@ function TokenCachePanel(props) {
355
358
  inputRate = num(model.cost.input);
356
359
  cacheReadRate = num(model.cost.cache?.read);
357
360
  cacheWriteRate = num(model.cost.cache?.write);
358
- const diff = inputRate - cacheReadRate;
359
- if (diff > 0)
360
- saved = (read * diff) / 1_000_000;
361
+ if (inputRate > cacheReadRate)
362
+ saved = (read * (inputRate - cacheReadRate)) / 1_000_000;
361
363
  break;
362
364
  }
363
- }
364
- // `input` from the API represents fresh (non-cached) tokens.
365
365
  const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0;
366
- // Total context = fresh + cache.read.
367
- const freshTotal = input + read;
368
- const sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0;
369
- const model = mid.split("/").pop() ?? mid;
370
- const hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
371
- let trend = 0;
366
+ const freshTotal = input + read, sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0;
367
+ const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
372
368
  const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
373
- if (hasTrendData) {
374
- trend = lastMsgHitRate - prevMsgHitRate;
375
- }
376
- const providerName = pid || "";
377
- // ── token distribution (in-process via api.state.part) ──
378
- // Wrapped in try-catch so a part fetching failure never crashes the panel.
379
- let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
380
- let hasDistData = false;
381
- try {
382
- partVersion(); // track part changes for reactivity
383
- dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
384
- // Read agent system prompt once before the message loop. Reading it
385
- // inside the per-user-message branch risks transient unavailability
386
- // (api.state.config not yet resolved during streaming) silently
387
- // resetting a previously-computed value and causing display flicker.
369
+ const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || "";
370
+ // untrack 只包裹已知触发死锁的 API
371
+ const distData = untrack(() => {
372
+ let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
373
+ let hasDistData = false;
388
374
  try {
389
- const session = props.api.state.session.get(props.sessionId);
390
- const cfg = props.api.state.config;
375
+ const session = props.api.state.session.get(sid), cfg = props.api.state.config;
391
376
  const agentName = String(session?.agent ?? cfg?.default_agent ?? "build");
392
377
  const agents = cfg?.agent;
393
378
  const agentCfg = agents?.[agentName];
394
379
  const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : "";
395
380
  if (sysPrompt)
396
381
  dist.system = estimateTokens(sysPrompt);
397
- }
398
- catch { }
399
- for (const msg of msgs) {
400
- if (msg.role === "user") {
401
- const um = msg;
402
- if (um.system)
403
- dist.system += estimateTokens(um.system);
404
- let parts = [];
405
- try {
406
- parts = props.api.state.part(msg.id);
407
- }
408
- catch { }
409
- for (const p of parts) {
410
- if (p.type === "text" && !p.synthetic && !p.ignored) {
411
- dist.user += estimateTokens(p.text);
382
+ let lastAssMsg;
383
+ for (const msg of msgs) {
384
+ if (msg.role === "user") {
385
+ const um = msg;
386
+ if (um.system)
387
+ dist.system += estimateTokens(um.system);
388
+ let parts = [];
389
+ try {
390
+ parts = props.api.state.part(msg.id);
412
391
  }
413
- else if (p.type === "file") {
414
- const fp = p;
415
- if (fp.source?.text?.value)
416
- dist.user += estimateTokens(fp.source.text.value);
392
+ catch { }
393
+ for (const p of parts) {
394
+ if (p.type === "text" && !p.synthetic && !p.ignored)
395
+ dist.user += estimateTokens(p.text);
396
+ else if (p.type === "file") {
397
+ const fp = p;
398
+ if (fp.source?.text?.value)
399
+ dist.user += estimateTokens(fp.source.text.value);
400
+ }
417
401
  }
418
402
  }
419
- }
420
- else if (msg.role === "assistant") {
421
- const am = msg;
422
- dist.output += num(am.tokens?.output);
423
- let parts = [];
424
- try {
425
- parts = props.api.state.part(msg.id);
426
- }
427
- catch { }
428
- for (const p of parts) {
429
- if (p.type === "tool") {
430
- const tp = p;
431
- // Tool call input (params)
432
- let rawInput = "";
433
- try {
434
- rawInput = tp.state.raw ?? JSON.stringify(tp.state.input);
435
- }
436
- catch {
403
+ else if (msg.role === "assistant") {
404
+ const am = msg;
405
+ dist.output += num(am.tokens?.output);
406
+ let parts = [];
407
+ try {
408
+ parts = props.api.state.part(msg.id);
409
+ }
410
+ catch { }
411
+ for (const p of parts) {
412
+ if (p.type === "tool") {
413
+ const tp = p;
414
+ let rawInput = "";
437
415
  try {
438
- rawInput = JSON.stringify(tp.state);
416
+ rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "");
439
417
  }
440
418
  catch { }
419
+ if (rawInput)
420
+ dist.toolCall += estimateTokens(rawInput);
421
+ if (tp.state.status === "completed") {
422
+ const c = tp.state;
423
+ if (c.output)
424
+ dist.toolResult += estimateTokens(c.output);
425
+ }
426
+ else if (tp.state.status === "error") {
427
+ const e = tp.state;
428
+ if (e.error)
429
+ dist.toolResult += estimateTokens(e.error);
430
+ }
441
431
  }
442
- if (rawInput)
443
- dist.toolCall += estimateTokens(rawInput);
444
- // Tool result output
445
- if (tp.state.status === "completed") {
446
- const completed = tp.state;
447
- if (completed.output)
448
- dist.toolResult += estimateTokens(completed.output);
449
- }
450
- else if (tp.state.status === "error") {
451
- const errored = tp.state;
452
- if (errored.error)
453
- dist.toolResult += estimateTokens(errored.error);
432
+ else if (p.type === "reasoning")
433
+ dist.agent += estimateTokens(p.text);
434
+ else if (p.type === "subtask") {
435
+ const sub = p;
436
+ dist.agent += estimateTokens(sub.prompt || sub.description || "");
454
437
  }
455
438
  }
456
- else if (p.type === "reasoning") {
457
- dist.agent += estimateTokens(p.text);
458
- }
459
- else if (p.type === "subtask") {
460
- const sub = p;
461
- dist.agent += estimateTokens(sub.prompt || sub.description || "");
462
- }
463
- else if (p.type === "step-finish") {
464
- // StepFinishPart carries API-exact per-call token counts.
465
- // Sum across all step-finish parts (one per API call in tool loops).
466
- const sf = p;
467
- dist.apiInput += sf.tokens?.input ?? 0;
468
- dist.apiOutput += sf.tokens?.output ?? 0;
469
- }
470
439
  }
471
440
  }
441
+ // 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
442
+ for (let i = msgs.length - 1; i >= 0; i--) {
443
+ if (msgs[i].role !== "assistant")
444
+ continue;
445
+ const t = msgs[i].tokens;
446
+ if (t && (t.input > 0 || (t.cache?.read ?? 0) > 0)) {
447
+ lastAssMsg = msgs[i];
448
+ break;
449
+ }
450
+ }
451
+ // 取最后一条有数据消息的总输入(含缓存读)作为当前 context 大小
452
+ dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read);
453
+ dist.apiOutput = num(lastAssMsg?.tokens?.output);
454
+ hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
472
455
  }
473
- const totalInput = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult;
474
- const apiTotalInput = dist.apiInput;
475
- // Use API output if available (StepFinishPart is more accurate than AssistantMessage.tokens)
476
- const finalOutput = dist.apiOutput > 0 ? dist.apiOutput : dist.output;
477
- // Gap inference: the SDK does not expose per-part token counts, so any
478
- // API-exact input total that exceeds the locally-estimated sum is attributed
479
- // to system prompt / agent config / tool-definition overhead. Add it to the
480
- // system bucket rather than replacing the local estimate.
481
- const overhead = Math.max(0, apiTotalInput - totalInput);
482
- if (overhead >= 50) {
483
- dist.system += overhead;
484
- }
485
- hasDistData = totalInput > 0 || finalOutput > 0 || apiTotalInput > 0;
486
- }
487
- catch {
488
- // Graceful degradation — dist stays at zeroes
489
- }
490
- // Fall back to last known-good distribution while api.state.part()
491
- // is re-hydrating after a view switch.
492
- const finalDist = hasDistData ? dist : lastDist();
493
- const finalHasDist = hasDistData || lastHasDist();
494
- return {
495
- hitRate, read, write, freshInput: input, output,
496
- cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
456
+ catch { }
457
+ const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
458
+ return { finalDist, finalHasDist };
459
+ });
460
+ setDataSignal({
461
+ hitRate, read, write, freshInput: input, output, cost, saved, model,
462
+ inputRate, cacheReadRate, cacheWriteRate, hasPricing,
497
463
  hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
498
- trend, hasTrendData,
499
- providerName,
500
- sessionHitRate,
501
- dist: finalDist,
502
- hasDistData: finalHasDist,
503
- };
464
+ trend, hasTrendData, providerName, sessionHitRate,
465
+ dist: distData.finalDist, hasDistData: distData.finalHasDist,
466
+ });
467
+ });
468
+ const data = createMemo(() => {
469
+ return dataSignal();
504
470
  });
505
471
  // Persist the last valid distribution so that data() can fall back
506
472
  // to it while api.state.part() is re-hydrating after a view switch.
@@ -600,8 +566,9 @@ function TokenCachePanel(props) {
600
566
  clearTimeout(partTimer);
601
567
  partTimer = setTimeout(() => setPartVersion((v) => v + 1), 100);
602
568
  };
603
- const unsubPart = props.api.event.on("message.part.updated", bumpPartVersion);
604
- const unsubMsg = props.api.event.on("message.updated", bumpPartVersion);
569
+ const unsubPart = props.api.event.on("message.part.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
570
+ const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
571
+ setRefreshTick(v => v + 1);
605
572
  onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); });
606
573
  });
607
574
  // ── colours ──
@@ -662,7 +629,7 @@ function TokenCachePanel(props) {
662
629
  // boxEl.width may be undefined before the first measurement — guard with 0
663
630
  const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
664
631
  setPanelWidth((prev) => (prev === w ? prev : w));
665
- }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: T.title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: pal().muted }, children: [" (v", PLUGIN_VERSION, ")"] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(T.title) - visualWidth(pct() + " " + T.hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", T.hitFolded] })] })] })] }), _jsx(Show, { when: open(), children: _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: T.noData })] })] }), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [T.hit, " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: data().hasTrendData, children: _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(T.totalHit, (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secDetail }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + T.secDetail)) })] }), _jsxs(Show, { when: detailOpen(), children: [_jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.read, fmt(data().read), T.tok) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.write, fmt(data().write), T.tok) }) }), _jsx("text", { fg: pal().muted, children: justify(T.miss, fmt(data().freshInput), T.tok) }), _jsx("text", { fg: pal().muted, children: justify(T.out, fmt(data().output), T.tok) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: T.saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(T.saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + T.secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(T.cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(T.provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(T.model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(T.rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + T.inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + T.cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + T.writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: T.distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + T.distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distSys, fmt(data().dist.system), T.tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distUser, fmt(data().dist.user), T.tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distAgent, fmt(data().dist.agent), T.tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distTool, fmt(data().dist.toolCall), T.tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(T.distRes, fmt(data().dist.toolResult), T.tok) }) }), _jsx("text", { fg: pal().text, children: justify(T.distTotal, fmt(data().dist.system + data().dist.user + data().dist.agent + data().dist.toolCall + data().dist.toolResult), T.tok) })] })] }) })] }) })] }));
632
+ }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: 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) })] })] }) })] }) })] }));
666
633
  }
667
634
  // ---------------------------------------------------------------------------
668
635
  // Plugin entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.9-beta.0",
3
+ "version": "1.2.10",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.2.9-beta.0";
2
+ export const PLUGIN_VERSION="1.2.10";