opencode-visual-cache 1.6.0 → 1.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -54,8 +54,11 @@
54
54
  "solid-js": ">=1.9.0"
55
55
  },
56
56
  "devDependencies": {
57
- "@opencode-ai/plugin": "^1.14.50",
58
- "@opencode-ai/sdk": "^1.14.50",
57
+ "@opencode-ai/plugin": "^1.18.16",
58
+ "@opencode-ai/sdk": "^1.18.16",
59
+ "@opentui/core": "^0.5.1",
60
+ "@opentui/solid": "^0.5.1",
61
+ "@opentui/keymap": "^0.5.1",
59
62
  "esbuild": "^0.25.0",
60
63
  "esbuild-plugin-solid": "^0.5.0",
61
64
  "tsx": "^4.22.3",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.6.0";
2
+ export const PLUGIN_VERSION="1.6.1";
package/src/i18n.ts CHANGED
@@ -27,11 +27,11 @@ const ZH_T = {
27
27
  distTitle: "估算 Token 分布",
28
28
  distSys: "系统提示:",
29
29
  distUser: "用户:",
30
- distAgent: "Agent 指令:",
30
+ distAgent: "子代理指令:",
31
31
  distTool: "Tool 调用:",
32
32
  distRes: "Tool 结果:",
33
- distTotal: "总计:",
34
- distOut: "输出:",
33
+ distReason: "推理:",
34
+ stepsCount: "本回合 {n} 次调用:",
35
35
  secDetail: "明细",
36
36
  secModel: "模型",
37
37
  secSkills: "已加载技能",
@@ -112,11 +112,11 @@ const EN_T: Translation = {
112
112
  distTitle: "Estimated Token Dist.",
113
113
  distSys: "System:",
114
114
  distUser: "User:",
115
- distAgent: "Agent Instr:",
115
+ distAgent: "Sub-Agent Instr:",
116
116
  distTool: "Tool Call:",
117
117
  distRes: "Tool Result:",
118
- distTotal: "Total:",
119
- distOut: "Output:",
118
+ distReason: "Reasoning:",
119
+ stepsCount: "Turn #{n} calls:",
120
120
  secDetail: "Detail",
121
121
  secModel: "Model",
122
122
  secSkills: "Loaded Skills",
@@ -194,11 +194,11 @@ const JA_T: Translation = {
194
194
  distTitle: "Token 分布(推定)",
195
195
  distSys: "システム:",
196
196
  distUser: "ユーザー:",
197
- distAgent: "Agent 指示:",
197
+ distAgent: "サブエージェント指示:",
198
198
  distTool: "Tool 呼び出し:",
199
199
  distRes: "Tool 結果:",
200
- distTotal: "合計:",
201
- distOut: "出力:",
200
+ distReason: "推論:",
201
+ stepsCount: "今回 {n} 回呼出:",
202
202
  secDetail: "明細",
203
203
  secModel: "モデル",
204
204
  secSkills: "読み込み済みスキル",
@@ -276,11 +276,11 @@ const KO_T: Translation = {
276
276
  distTitle: "Token 분포(추정)",
277
277
  distSys: "시스템:",
278
278
  distUser: "사용자:",
279
- distAgent: "Agent 지시:",
279
+ distAgent: "서브 에이전트 지시:",
280
280
  distTool: "Tool 호출:",
281
281
  distRes: "Tool 결과:",
282
- distTotal: "합계:",
283
- distOut: "출력:",
282
+ distReason: "추론:",
283
+ stepsCount: "이번 턴 {n}회 호출:",
284
284
  secDetail: "상세",
285
285
  secModel: "모델",
286
286
  secSkills: "로드된 스킬",
package/src/index.tsx CHANGED
@@ -259,13 +259,15 @@ function estimateTokens(text: string): number {
259
259
  interface TokenDist {
260
260
  system: number // UserMessage.system
261
261
  user: number // user message text/file parts
262
- agent: number // SubtaskPart.prompt + ReasoningPart.text
262
+ agent: number // task tool input prompt/description (sub-agent delegation)
263
263
  toolCall: number // ToolPart.input (actual tool params)
264
264
  toolResult: number // ToolPart completed output / error
265
- output: number // AssistantMessage.tokens.output (fallback)
265
+ output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
266
+ reasoning: number // AssistantMessage.tokens.reasoning (API exact)
266
267
  apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
267
268
  apiInput: number // API exact total input context (input + cache read + cache write)
268
- stepCost: number
269
+ stepCost: number // last step-finish part cost (USD) in the current round
270
+ stepCount: number // step-finish parts count across the current round (parentID chain)
269
271
  }
270
272
 
271
273
  // ---------------------------------------------------------------------------
@@ -472,7 +474,7 @@ function TokenCachePanel(props: {
472
474
  // stable until the next successful computation arrives.
473
475
  const [lastDist, setLastDist] = createSignal<TokenDist>({
474
476
  system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
475
- output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
477
+ output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0,
476
478
  })
477
479
  const [lastHasDist, setLastHasDist] = createSignal(false)
478
480
 
@@ -481,7 +483,7 @@ function TokenCachePanel(props: {
481
483
  cost: 0, saved: 0, model: "", inputRate: 0, cacheReadRate: 0, cacheWriteRate: 0,
482
484
  hasPricing: false, hasData: false, trend: 0, hasTrendData: false,
483
485
  providerName: "", sessionHitRate: 0,
484
- dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
486
+ dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 },
485
487
  hasDistData: false,
486
488
  skills: [] as { name: string; tokens: number }[],
487
489
  hasSkills: false,
@@ -599,7 +601,7 @@ function TokenCachePanel(props: {
599
601
 
600
602
  // untrack 只包裹已知触发死锁的 API
601
603
  const distData = untrack(() => {
602
- let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 }
604
+ let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 }
603
605
  let hasDistData = false
604
606
  const loadedSkills = new Map<string, { name: string; tokens: number }>()
605
607
  try {
@@ -621,12 +623,20 @@ function TokenCachePanel(props: {
621
623
  } else if (msg.role === "assistant") {
622
624
  const am = msg as AssistantMessage
623
625
  dist.output += num(am.tokens?.output)
626
+ dist.reasoning += num(am.tokens?.reasoning)
624
627
  let parts: readonly Part[] = []; try { parts = props.api.state.part(msg.id) } catch {}
625
628
  for (const p of parts) {
626
629
  if (p.type === "tool") {
627
630
  const tp = p as any; let rawInput = ""
628
631
  try { rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "") } catch {}
629
632
  if (rawInput) dist.toolCall += estimateTokens(rawInput)
633
+ // 子代理委托(task 工具):任务描述计入子代理指令(1.15.x 无 subtask part)
634
+ if (tp.tool === "task" && tp.state?.input) {
635
+ const ti = tp.state.input
636
+ const prompt = typeof ti.prompt === "string" ? ti.prompt : ""
637
+ const desc = typeof ti.description === "string" ? ti.description : ""
638
+ dist.agent += estimateTokens(prompt || desc)
639
+ }
630
640
  if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
631
641
  else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
632
642
  if (tp.tool === "skill" && tp.state.status === "completed") {
@@ -647,8 +657,7 @@ function TokenCachePanel(props: {
647
657
  }
648
658
  }
649
659
  }
650
- } else if (p.type === "reasoning") dist.agent += estimateTokens((p as any).text)
651
- else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
660
+ } else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
652
661
  }
653
662
  }
654
663
  }
@@ -661,7 +670,27 @@ function TokenCachePanel(props: {
661
670
  // 取最后一条有数据消息的总输入(含缓存读/写)作为当前 context 大小
662
671
  dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read) + num(lastAssMsg?.tokens?.cache?.write)
663
672
  dist.apiOutput = num(lastAssMsg?.tokens?.output)
664
- hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0
673
+ // 本回合(最后一条有数据消息所在的 parentID 链)的 API 调用次数与末次成本。
674
+ // opencode 将回合内每次工具调用循环拆为独立 assistant 消息(各含 1 个 step-finish),
675
+ // 故按 parentID 链聚合统计,而非单条消息。
676
+ if (lastAssMsg) {
677
+ const roundParent = (lastAssMsg as AssistantMessage).parentID
678
+ let lastCost: number | undefined
679
+ for (let i = msgs.length - 1; i >= 0; i--) {
680
+ const m = msgs[i]
681
+ if (m.role !== "assistant") continue
682
+ if ((m as AssistantMessage).parentID !== roundParent) break
683
+ let parts: readonly Part[] = []; try { parts = props.api.state.part(m.id) } catch {}
684
+ for (const p of parts) {
685
+ if (p.type !== "step-finish") continue
686
+ dist.stepCount++
687
+ const sc = (p as { cost?: unknown }).cost
688
+ if (lastCost === undefined && typeof sc === "number" && Number.isFinite(sc)) lastCost = sc
689
+ }
690
+ }
691
+ if (lastCost !== undefined) dist.stepCost = lastCost
692
+ }
693
+ hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0 || dist.reasoning > 0
665
694
  } catch {}
666
695
  const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
667
696
  const skills = [...loadedSkills.values()]
@@ -976,6 +1005,12 @@ function TokenCachePanel(props: {
976
1005
  <text fg={pal().muted}>
977
1006
  {justify(t("out"), fmt(data().output), t("tok"))}
978
1007
  </text>
1008
+ {/* 本回合多次 API 调用时才显示调用次数与末次成本(单次调用不占行) */}
1009
+ <Show when={data().dist.stepCount >= 2}>
1010
+ <text fg={pal().muted}>
1011
+ {justify(t("stepsCount", { n: data().dist.stepCount }), fmtCost(data().dist.stepCost, currencySymbol(), exchangeRate()))}
1012
+ </text>
1013
+ </Show>
979
1014
  <Show when={data().saved > 0}>
980
1015
  <text>
981
1016
  <span style={{ fg: pal().muted }}>{t("saved")}</span>
@@ -1058,9 +1093,11 @@ function TokenCachePanel(props: {
1058
1093
  {justify(t("distRes"), fmt(data().dist.toolResult), t("tok"))}
1059
1094
  </text>
1060
1095
  </Show>
1061
- <text fg={pal().text}>
1062
- {justify(t("distTotal"), fmt(data().dist.apiInput), t("tok"))}
1063
- </text>
1096
+ <Show when={data().dist.reasoning > 0}>
1097
+ <text fg={pal().muted}>
1098
+ {justify(t("distReason"), fmt(data().dist.reasoning), t("tok"))}
1099
+ </text>
1100
+ </Show>
1064
1101
  </Show>
1065
1102
  </Show>
1066
1103
  </Show>