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

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.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.9-beta.0";
1
+ export declare const PLUGIN_VERSION = "1.2.9";
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.9";
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
@@ -306,29 +306,33 @@ function TokenCachePanel(props) {
306
306
  output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
307
307
  });
308
308
  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;
309
+ const [dataSignal, setDataSignal] = createSignal({
310
+ hitRate: 0, read: 0, write: 0, freshInput: 0, output: 0,
311
+ cost: 0, saved: 0, model: "", inputRate: 0, cacheReadRate: 0, cacheWriteRate: 0,
312
+ hasPricing: false, hasData: false, trend: 0, hasTrendData: false,
313
+ providerName: "", sessionHitRate: 0,
314
+ dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
315
+ hasDistData: false,
316
+ });
317
+ const [refreshTick, setRefreshTick] = createSignal(0);
318
+ createEffect(() => {
319
+ const sid = props.sessionId;
320
+ void refreshTick();
321
+ void partVersion();
322
+ // 自然追踪 messages 和 provider(SDK 数据就绪时自动重新执行)
323
+ const msgs = props.api.state.session.messages(sid);
324
+ let input = 0, read = 0, write = 0, output = 0, cost = 0, pid = "", mid = "";
325
+ let prevMsgHitRate = -1, lastMsgHitRate = -1;
321
326
  for (const msg of msgs) {
322
327
  if (msg.role !== "assistant")
323
328
  continue;
324
329
  const t = msg.tokens;
325
330
  if (!t)
326
331
  continue;
327
- const msgInputTokens = num(t.input) + num(t.cache?.read);
328
- const msgReadTokens = num(t.cache?.read);
329
- if (msgInputTokens > 0) {
332
+ const mit = num(t.input) + num(t.cache?.read), mrt = num(t.cache?.read);
333
+ if (mit > 0) {
330
334
  prevMsgHitRate = lastMsgHitRate;
331
- lastMsgHitRate = (msgReadTokens / msgInputTokens) * 100;
335
+ lastMsgHitRate = (mrt / mit) * 100;
332
336
  }
333
337
  input += num(t.input);
334
338
  read += num(t.cache?.read);
@@ -340,12 +344,8 @@ function TokenCachePanel(props) {
340
344
  mid = msg.modelID;
341
345
  }
342
346
  }
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) {
347
+ let saved = 0, inputRate = 0, cacheReadRate = 0, cacheWriteRate = 0;
348
+ if (read > 0 && pid && mid)
349
349
  for (const provider of props.api.state.provider) {
350
350
  if (provider.id !== pid)
351
351
  continue;
@@ -355,152 +355,115 @@ function TokenCachePanel(props) {
355
355
  inputRate = num(model.cost.input);
356
356
  cacheReadRate = num(model.cost.cache?.read);
357
357
  cacheWriteRate = num(model.cost.cache?.write);
358
- const diff = inputRate - cacheReadRate;
359
- if (diff > 0)
360
- saved = (read * diff) / 1_000_000;
358
+ if (inputRate > cacheReadRate)
359
+ saved = (read * (inputRate - cacheReadRate)) / 1_000_000;
361
360
  break;
362
361
  }
363
- }
364
- // `input` from the API represents fresh (non-cached) tokens.
365
362
  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;
363
+ const freshTotal = input + read, sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0;
364
+ const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
372
365
  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.
366
+ const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || "";
367
+ // untrack 只包裹已知触发死锁的 API
368
+ const distData = untrack(() => {
369
+ let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
370
+ let hasDistData = false;
388
371
  try {
389
- const session = props.api.state.session.get(props.sessionId);
390
- const cfg = props.api.state.config;
372
+ const session = props.api.state.session.get(sid), cfg = props.api.state.config;
391
373
  const agentName = String(session?.agent ?? cfg?.default_agent ?? "build");
392
374
  const agents = cfg?.agent;
393
375
  const agentCfg = agents?.[agentName];
394
376
  const sysPrompt = typeof agentCfg?.prompt === "string" ? agentCfg.prompt : "";
395
377
  if (sysPrompt)
396
378
  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);
379
+ for (const msg of msgs) {
380
+ if (msg.role === "user") {
381
+ const um = msg;
382
+ if (um.system)
383
+ dist.system += estimateTokens(um.system);
384
+ let parts = [];
385
+ try {
386
+ parts = props.api.state.part(msg.id);
412
387
  }
413
- else if (p.type === "file") {
414
- const fp = p;
415
- if (fp.source?.text?.value)
416
- dist.user += estimateTokens(fp.source.text.value);
388
+ catch { }
389
+ for (const p of parts) {
390
+ if (p.type === "text" && !p.synthetic && !p.ignored)
391
+ dist.user += estimateTokens(p.text);
392
+ else if (p.type === "file") {
393
+ const fp = p;
394
+ if (fp.source?.text?.value)
395
+ dist.user += estimateTokens(fp.source.text.value);
396
+ }
417
397
  }
418
398
  }
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 {
399
+ else if (msg.role === "assistant") {
400
+ const am = msg;
401
+ dist.output += num(am.tokens?.output);
402
+ let parts = [];
403
+ try {
404
+ parts = props.api.state.part(msg.id);
405
+ }
406
+ catch { }
407
+ for (const p of parts) {
408
+ if (p.type === "tool") {
409
+ const tp = p;
410
+ let rawInput = "";
437
411
  try {
438
- rawInput = JSON.stringify(tp.state);
412
+ rawInput = tp.state.raw ?? JSON.stringify(tp.state.input);
413
+ }
414
+ catch {
415
+ try {
416
+ rawInput = JSON.stringify(tp.state);
417
+ }
418
+ catch { }
419
+ }
420
+ if (rawInput)
421
+ dist.toolCall += estimateTokens(rawInput);
422
+ if (tp.state.status === "completed") {
423
+ const c = tp.state;
424
+ if (c.output)
425
+ dist.toolResult += estimateTokens(c.output);
426
+ }
427
+ else if (tp.state.status === "error") {
428
+ const e = tp.state;
429
+ if (e.error)
430
+ dist.toolResult += estimateTokens(e.error);
439
431
  }
440
- catch { }
441
432
  }
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);
433
+ else if (p.type === "reasoning")
434
+ dist.agent += estimateTokens(p.text);
435
+ else if (p.type === "subtask") {
436
+ const sub = p;
437
+ dist.agent += estimateTokens(sub.prompt || sub.description || "");
449
438
  }
450
- else if (tp.state.status === "error") {
451
- const errored = tp.state;
452
- if (errored.error)
453
- dist.toolResult += estimateTokens(errored.error);
439
+ else if (p.type === "step-finish") {
440
+ const sf = p;
441
+ dist.apiInput += sf.tokens?.input ?? 0;
442
+ dist.apiOutput += sf.tokens?.output ?? 0;
454
443
  }
455
444
  }
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
445
  }
471
446
  }
447
+ const totalInput = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult;
448
+ const overhead = Math.max(0, dist.apiInput - totalInput);
449
+ if (overhead >= 50)
450
+ dist.system += overhead;
451
+ hasDistData = totalInput > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
472
452
  }
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,
453
+ catch { }
454
+ const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
455
+ return { finalDist, finalHasDist };
456
+ });
457
+ setDataSignal({
458
+ hitRate, read, write, freshInput: input, output, cost, saved, model,
459
+ inputRate, cacheReadRate, cacheWriteRate, hasPricing,
497
460
  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
- };
461
+ trend, hasTrendData, providerName, sessionHitRate,
462
+ dist: distData.finalDist, hasDistData: distData.finalHasDist,
463
+ });
464
+ });
465
+ const data = createMemo(() => {
466
+ return dataSignal();
504
467
  });
505
468
  // Persist the last valid distribution so that data() can fall back
506
469
  // to it while api.state.part() is re-hydrating after a view switch.
@@ -600,8 +563,9 @@ function TokenCachePanel(props) {
600
563
  clearTimeout(partTimer);
601
564
  partTimer = setTimeout(() => setPartVersion((v) => v + 1), 100);
602
565
  };
603
- const unsubPart = props.api.event.on("message.part.updated", bumpPartVersion);
604
- const unsubMsg = props.api.event.on("message.updated", bumpPartVersion);
566
+ const unsubPart = props.api.event.on("message.part.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
567
+ const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
568
+ setRefreshTick(v => v + 1);
605
569
  onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); });
606
570
  });
607
571
  // ── colours ──
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.9",
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.9";