opencode-visual-cache 1.7.0-beta.3 → 1.7.0-beta.5

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,104 +1,104 @@
1
- import type { Message, Part, Session } from "@opencode-ai/sdk"
2
- import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
3
- import type { BalanceEntry } from "../balance-providers"
4
- import type { LangCode } from "../i18n"
5
-
6
- /** 会话信息(面板消费的字段;SDK Session 类型过严,用宽松接口) */
7
- export interface PanelSession {
8
- id: string
9
- title?: string
10
- agent?: string
11
- model?: { providerID?: string; id?: string }
12
- tokens?: { input?: number; output?: number; reasoning?: number; cache?: { read?: number; write?: number } }
13
- cost?: number
14
- [key: string]: unknown
15
- }
16
-
17
- /**
18
- * Panel API 契约:TokenCachePanel 消费的宿主 API 子集。
19
- * V1(TuiPluginApi)天然满足;V2(opencode2 context)由 v2-panel-api 适配实现。
20
- */
21
- export interface PanelApi {
22
- kv: {
23
- ready: boolean
24
- get<T>(key: string, fallback?: T): T | undefined
25
- set(key: string, value: unknown): void | Promise<void>
26
- }
27
- state: {
28
- session: {
29
- get(id: string): PanelSession | undefined
30
- /** V1 返回 sdk/v2 的 Message、V2 返回 SessionMessageInfo——统一放宽 */
31
- messages(id: string): readonly any[]
32
- }
33
- provider: readonly Record<string, any>[]
34
- config: any
35
- part(messageID: string): readonly any[]
36
- path: { directory: string }
37
- }
38
- event: {
39
- on(type: string, handler: (event: unknown) => void): () => void
40
- }
41
- renderer: { terminalWidth: number }
42
- keys: { formatBindings(binding: unknown): string | undefined }
43
- tuiConfig: { keybinds: { get(command: string): unknown } }
44
- }
45
-
46
- export interface BalanceState {
47
- status: "idle" | "loading" | "ok" | "error"
48
- data: BalanceEntry[] | null
49
- lastFetch: number
50
- error?: string
51
- key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
52
- }
53
-
54
- /** Signals shared between the TUI component and slash commands.
55
- * Created in the `tui` function scope so they do not survive module reload —
56
- * the component re-creates them on mount and restores user config from kv. */
57
- export interface PanelSignals {
58
- currencySymbol: () => string
59
- setCurrencySymbol: (v: string) => void
60
- exchangeRate: () => number
61
- setExchangeRate: (v: number) => void
62
- langCode: () => LangCode
63
- setLangCode: (v: LangCode) => void
64
- sectionDetail: () => boolean
65
- setSectionDetail: (v: boolean) => void
66
- sectionModel: () => boolean
67
- setSectionModel: (v: boolean) => void
68
- sectionDist: () => boolean
69
- setSectionDist: (v: boolean) => void
70
- sectionSkills: () => boolean
71
- setSectionSkills: (v: boolean) => void
72
- sectionBalance: () => boolean
73
- setSectionBalance: (v: boolean) => void
74
- /** Bottom status bar (prompt hint line) visibility. */
75
- sectionBottom: () => boolean
76
- setSectionBottom: (v: boolean) => void
77
- /** Increment to force a balance re-fetch. */
78
- balanceRefresh: () => number
79
- setBalanceRefresh: (v: number) => void
80
- /** Currently selected balance provider id (e.g. "deepseek"). */
81
- balanceProviderId: () => string
82
- setBalanceProviderId: (v: string) => void
83
- /** Auto-switch to the session's provider for balance display. Manual switch disables it. */
84
- autoBalance: () => boolean
85
- setAutoBalance: (v: boolean) => void
86
- /** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
87
- balanceUnsupported: () => boolean
88
- setBalanceUnsupported: (v: boolean) => void
89
- /** Shared balance query state — single source of truth for sidebar and bottom bar. */
90
- balanceState: () => BalanceState
91
- /** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
92
- balanceCurrency: () => string
93
- setBalanceCurrency: (v: string) => void
94
- borderVisible: () => boolean
95
- setBorderVisible: (v: boolean) => void
96
- /** When set, the panel renders stats for this session instead of the main one. */
97
- overrideSessionId: () => string | undefined
98
- setOverrideSessionId: (v: string | undefined) => void
99
- /** True while our sidebar panel is mounted — host sidebar is visible (occupies 42 cols). */
100
- sidebarVisible: () => boolean
101
- setSidebarVisible: (v: boolean) => void
102
- }
103
-
104
- export type { TuiThemeCurrent }
1
+ import type { Message, Part, Session } from "@opencode-ai/sdk"
2
+ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
3
+ import type { BalanceEntry } from "../balance-providers"
4
+ import type { LangCode } from "../i18n"
5
+
6
+ /** 会话信息(面板消费的字段;SDK Session 类型过严,用宽松接口) */
7
+ export interface PanelSession {
8
+ id: string
9
+ title?: string
10
+ agent?: string
11
+ model?: { providerID?: string; id?: string }
12
+ tokens?: { input?: number; output?: number; reasoning?: number; cache?: { read?: number; write?: number } }
13
+ cost?: number
14
+ [key: string]: unknown
15
+ }
16
+
17
+ /**
18
+ * Panel API 契约:TokenCachePanel 消费的宿主 API 子集。
19
+ * V1(TuiPluginApi)天然满足;V2(opencode2 context)由 v2-panel-api 适配实现。
20
+ */
21
+ export interface PanelApi {
22
+ kv: {
23
+ ready: boolean
24
+ get<T>(key: string, fallback?: T): T | undefined
25
+ set(key: string, value: unknown): void | Promise<void>
26
+ }
27
+ state: {
28
+ session: {
29
+ get(id: string): PanelSession | undefined
30
+ /** V1 返回 sdk/v2 的 Message、V2 返回 SessionMessageInfo——统一放宽 */
31
+ messages(id: string): readonly any[]
32
+ }
33
+ provider: readonly Record<string, any>[]
34
+ config: any
35
+ part(messageID: string): readonly any[]
36
+ path: { directory: string }
37
+ }
38
+ event: {
39
+ on(type: string, handler: (event: unknown) => void): () => void
40
+ }
41
+ renderer: { terminalWidth: number }
42
+ keys: { formatBindings(binding: unknown): string | undefined }
43
+ tuiConfig: { keybinds: { get(command: string): unknown } }
44
+ }
45
+
46
+ export interface BalanceState {
47
+ status: "idle" | "loading" | "ok" | "error"
48
+ data: BalanceEntry[] | null
49
+ lastFetch: number
50
+ error?: string
51
+ key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
52
+ }
53
+
54
+ /** Signals shared between the TUI component and slash commands.
55
+ * Created in the `tui` function scope so they do not survive module reload —
56
+ * the component re-creates them on mount and restores user config from kv. */
57
+ export interface PanelSignals {
58
+ currencySymbol: () => string
59
+ setCurrencySymbol: (v: string) => void
60
+ exchangeRate: () => number
61
+ setExchangeRate: (v: number) => void
62
+ langCode: () => LangCode
63
+ setLangCode: (v: LangCode) => void
64
+ sectionDetail: () => boolean
65
+ setSectionDetail: (v: boolean) => void
66
+ sectionModel: () => boolean
67
+ setSectionModel: (v: boolean) => void
68
+ sectionDist: () => boolean
69
+ setSectionDist: (v: boolean) => void
70
+ sectionSkills: () => boolean
71
+ setSectionSkills: (v: boolean) => void
72
+ sectionBalance: () => boolean
73
+ setSectionBalance: (v: boolean) => void
74
+ /** Bottom status bar (prompt hint line) visibility. */
75
+ sectionBottom: () => boolean
76
+ setSectionBottom: (v: boolean) => void
77
+ /** Increment to force a balance re-fetch. */
78
+ balanceRefresh: () => number
79
+ setBalanceRefresh: (v: number) => void
80
+ /** Currently selected balance provider id (e.g. "deepseek"). */
81
+ balanceProviderId: () => string
82
+ setBalanceProviderId: (v: string) => void
83
+ /** Auto-switch to the session's provider for balance display. Manual switch disables it. */
84
+ autoBalance: () => boolean
85
+ setAutoBalance: (v: boolean) => void
86
+ /** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
87
+ balanceUnsupported: () => boolean
88
+ setBalanceUnsupported: (v: boolean) => void
89
+ /** Shared balance query state — single source of truth for sidebar and bottom bar. */
90
+ balanceState: () => BalanceState
91
+ /** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
92
+ balanceCurrency: () => string
93
+ setBalanceCurrency: (v: string) => void
94
+ borderVisible: () => boolean
95
+ setBorderVisible: (v: boolean) => void
96
+ /** When set, the panel renders stats for this session instead of the main one. */
97
+ overrideSessionId: () => string | undefined
98
+ setOverrideSessionId: (v: string | undefined) => void
99
+ /** True while our sidebar panel is mounted — host sidebar is visible (occupies 42 cols). */
100
+ sidebarVisible: () => boolean
101
+ setSidebarVisible: (v: boolean) => void
102
+ }
103
+
104
+ export type { TuiThemeCurrent }
package/src/v2/data.ts CHANGED
@@ -1,112 +1,112 @@
1
- import type { Context, MessageInfo } from "./types"
2
- import { num } from "../core"
3
-
4
- /** 汇总一个会话的 token 用量与费用(V2 adapter → 同构模型)。 */
5
- export interface SessionStats {
6
- input: number
7
- output: number
8
- cacheRead: number
9
- cacheWrite: number
10
- reasoning: number
11
- cost: number
12
- hasData: boolean
13
- }
14
-
15
- /** 从 messages 汇总 token:统计全部 assistant 消息的 API 精确值。 */
16
- export function collectSessionStats(context: Context, sessionID: string): SessionStats {
17
- const msgs = context.data.session.message.list(sessionID) ?? []
18
- let input = 0
19
- let output = 0
20
- let cacheRead = 0
21
- let cacheWrite = 0
22
- let reasoning = 0
23
- let cost = 0
24
- let hasData = false
25
-
26
- for (const m of msgs) {
27
- if (m.type !== "assistant") continue
28
- const t = m.tokens
29
- if (!t) continue
30
- input += num(t.input)
31
- output += num(t.output)
32
- reasoning += num(t.reasoning)
33
- cacheRead += num(t.cache?.read)
34
- cacheWrite += num(t.cache?.write)
35
- hasData = true
36
- }
37
- // 会话级费用优先(V2 提供 cost()),消息级 cost 兜底(结构未知,宽松读取)
38
- const sessionCost = context.data.session.cost(sessionID)
39
- if (Number.isFinite(sessionCost) && sessionCost > 0) cost = sessionCost
40
- else {
41
- for (const m of msgs) {
42
- if (m.type !== "assistant" || m.cost == null) continue
43
- const c = m.cost as { amount?: unknown; total?: unknown } | { amount?: unknown } | number
44
- if (typeof c === "number" && Number.isFinite(c)) cost += c
45
- else {
46
- const amt = (c as { amount?: unknown }).amount
47
- if (typeof amt === "number" && Number.isFinite(amt)) cost += amt
48
- else if (typeof amt === "string") {
49
- const n = parseFloat(amt)
50
- if (Number.isFinite(n)) cost += n
51
- }
52
- }
53
- }
54
- }
55
- return { input, output, cacheRead, cacheWrite, reasoning, cost, hasData }
56
- }
57
-
58
- /** 缓存命中率(0–100):缓存读 /(新鲜输入 + 缓存读 + 缓存写),与 V1 口径一致。 */
59
- export function hitRate(stats: SessionStats): number {
60
- const denom = stats.input + stats.cacheRead + stats.cacheWrite
61
- if (denom <= 0) return 0
62
- return (stats.cacheRead / denom) * 100
63
- }
64
-
65
- /** 输入侧总量(底部栏口径,不含输出)。 */
66
- export function inputTotal(stats: SessionStats): number {
67
- return stats.input + stats.cacheRead + stats.cacheWrite
68
- }
69
-
70
- /** 单条命中率(V1 底部栏口径):最后一条有 token 的 assistant 消息,
71
- * 分母含缓存写 read/(input+read+write);prevHitRate 为上一条(趋势用)。 */
72
- export function collectLastHitRate(context: Context, sessionID: string): {
73
- hitRate: number
74
- prevHitRate: number
75
- input: number
76
- read: number
77
- write: number
78
- } {
79
- const msgs = context.data.session.message.list(sessionID) ?? []
80
- const session = context.data.session.get(sessionID) as { tokens?: { input?: number; cache?: { read?: number; write?: number } } } | undefined
81
- let input = num(session?.tokens?.input)
82
- let read = num(session?.tokens?.cache?.read)
83
- let write = num(session?.tokens?.cache?.write)
84
- // 无 session 聚合字段 → 遍历消息累加(与 V1 fallback 一致)
85
- if (session?.tokens == null) {
86
- for (const m of msgs) {
87
- if (m.type !== "assistant") continue
88
- const t = m.tokens
89
- if (!t) continue
90
- input += num(t.input)
91
- read += num(t.cache?.read)
92
- write += num(t.cache?.write)
93
- }
94
- }
95
- let hitRate = -1, prevHitRate = -1
96
- for (let i = msgs.length - 1; i >= 0; i--) {
97
- const m = msgs[i]
98
- if (m.type !== "assistant") continue
99
- const tk = m.tokens
100
- if (!tk) continue
101
- const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
102
- const mrt = num(tk.cache?.read)
103
- if (mit <= 0) continue
104
- const rate = (mrt / mit) * 100
105
- if (hitRate < 0) { hitRate = rate; continue }
106
- prevHitRate = rate
107
- break
108
- }
109
- return { hitRate, prevHitRate, input, read, write }
110
- }
111
-
112
- export type { MessageInfo }
1
+ import type { Context, MessageInfo } from "./types"
2
+ import { num } from "../core"
3
+
4
+ /** 汇总一个会话的 token 用量与费用(V2 adapter → 同构模型)。 */
5
+ export interface SessionStats {
6
+ input: number
7
+ output: number
8
+ cacheRead: number
9
+ cacheWrite: number
10
+ reasoning: number
11
+ cost: number
12
+ hasData: boolean
13
+ }
14
+
15
+ /** 从 messages 汇总 token:统计全部 assistant 消息的 API 精确值。 */
16
+ export function collectSessionStats(context: Context, sessionID: string): SessionStats {
17
+ const msgs = context.data.session.message.list(sessionID) ?? []
18
+ let input = 0
19
+ let output = 0
20
+ let cacheRead = 0
21
+ let cacheWrite = 0
22
+ let reasoning = 0
23
+ let cost = 0
24
+ let hasData = false
25
+
26
+ for (const m of msgs) {
27
+ if (m.type !== "assistant") continue
28
+ const t = m.tokens
29
+ if (!t) continue
30
+ input += num(t.input)
31
+ output += num(t.output)
32
+ reasoning += num(t.reasoning)
33
+ cacheRead += num(t.cache?.read)
34
+ cacheWrite += num(t.cache?.write)
35
+ hasData = true
36
+ }
37
+ // 会话级费用优先(V2 提供 cost()),消息级 cost 兜底(结构未知,宽松读取)
38
+ const sessionCost = context.data.session.cost(sessionID)
39
+ if (Number.isFinite(sessionCost) && sessionCost > 0) cost = sessionCost
40
+ else {
41
+ for (const m of msgs) {
42
+ if (m.type !== "assistant" || m.cost == null) continue
43
+ const c = m.cost as { amount?: unknown; total?: unknown } | { amount?: unknown } | number
44
+ if (typeof c === "number" && Number.isFinite(c)) cost += c
45
+ else {
46
+ const amt = (c as { amount?: unknown }).amount
47
+ if (typeof amt === "number" && Number.isFinite(amt)) cost += amt
48
+ else if (typeof amt === "string") {
49
+ const n = parseFloat(amt)
50
+ if (Number.isFinite(n)) cost += n
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return { input, output, cacheRead, cacheWrite, reasoning, cost, hasData }
56
+ }
57
+
58
+ /** 缓存命中率(0–100):缓存读 /(新鲜输入 + 缓存读 + 缓存写),与 V1 口径一致。 */
59
+ export function hitRate(stats: SessionStats): number {
60
+ const denom = stats.input + stats.cacheRead + stats.cacheWrite
61
+ if (denom <= 0) return 0
62
+ return (stats.cacheRead / denom) * 100
63
+ }
64
+
65
+ /** 输入侧总量(底部栏口径,不含输出)。 */
66
+ export function inputTotal(stats: SessionStats): number {
67
+ return stats.input + stats.cacheRead + stats.cacheWrite
68
+ }
69
+
70
+ /** 单条命中率(V1 底部栏口径):最后一条有 token 的 assistant 消息,
71
+ * 分母含缓存写 read/(input+read+write);prevHitRate 为上一条(趋势用)。 */
72
+ export function collectLastHitRate(context: Context, sessionID: string): {
73
+ hitRate: number
74
+ prevHitRate: number
75
+ input: number
76
+ read: number
77
+ write: number
78
+ } {
79
+ const msgs = context.data.session.message.list(sessionID) ?? []
80
+ const session = context.data.session.get(sessionID) as { tokens?: { input?: number; cache?: { read?: number; write?: number } } } | undefined
81
+ let input = num(session?.tokens?.input)
82
+ let read = num(session?.tokens?.cache?.read)
83
+ let write = num(session?.tokens?.cache?.write)
84
+ // 无 session 聚合字段 → 遍历消息累加(与 V1 fallback 一致)
85
+ if (session?.tokens == null) {
86
+ for (const m of msgs) {
87
+ if (m.type !== "assistant") continue
88
+ const t = m.tokens
89
+ if (!t) continue
90
+ input += num(t.input)
91
+ read += num(t.cache?.read)
92
+ write += num(t.cache?.write)
93
+ }
94
+ }
95
+ let hitRate = -1, prevHitRate = -1
96
+ for (let i = msgs.length - 1; i >= 0; i--) {
97
+ const m = msgs[i]
98
+ if (m.type !== "assistant") continue
99
+ const tk = m.tokens
100
+ if (!tk) continue
101
+ const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
102
+ const mrt = num(tk.cache?.read)
103
+ if (mit <= 0) continue
104
+ const rate = (mrt / mit) * 100
105
+ if (hitRate < 0) { hitRate = rate; continue }
106
+ prevHitRate = rate
107
+ break
108
+ }
109
+ return { hitRate, prevHitRate, input, read, write }
110
+ }
111
+
112
+ export type { MessageInfo }