opencode-visual-cache 1.7.0-beta.3 → 1.7.0-beta.4
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/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/balance-providers.js +27 -1
- package/dist/index.js +4 -1
- package/dist/tui.js +31 -2
- package/dist/v2.js +23 -2
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +25 -1
- package/src/core/color.ts +108 -108
- package/src/core/currency.ts +46 -46
- package/src/core/estimate.ts +36 -36
- package/src/core/format.ts +81 -81
- package/src/core/index.ts +5 -5
- package/src/core/types.ts +17 -17
- package/src/index.tsx +3 -0
- package/src/panel/panel-api.ts +104 -104
- package/src/v2/data.ts +112 -112
- package/src/v2/index.tsx +187 -187
- package/src/v2/sidebar.tsx +69 -69
- package/src/v2/status.tsx +96 -96
- package/src/v2/theme.ts +19 -19
- package/src/v2/types.ts +159 -159
- package/src/v2/v2-panel-api.ts +177 -177
- package/tui/index.js +1 -1
package/src/core/format.ts
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/** CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
2
|
-
* string length (=1 per char), which breaks alignment with mixed text. */
|
|
3
|
-
|
|
4
|
-
export function charColumns(c: string): number {
|
|
5
|
-
const code = c.codePointAt(0) ?? 0
|
|
6
|
-
if (code < 0x20) return 0 // control
|
|
7
|
-
if (code < 0x7F) return 1 // ASCII
|
|
8
|
-
if (code < 0xA0) return 0 // C1 controls
|
|
9
|
-
// East-Asian wide / fullwidth ranges
|
|
10
|
-
if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
|
|
11
|
-
(code >= 0x2E80 && code <= 0xA4CF) || // CJK Radicals … Yi
|
|
12
|
-
(code >= 0xAC00 && code <= 0xD7A3) || // Hangul
|
|
13
|
-
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compat
|
|
14
|
-
(code >= 0xFE10 && code <= 0xFE6F) || // Vertical / Compat
|
|
15
|
-
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth
|
|
16
|
-
(code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth signs
|
|
17
|
-
(code >= 0x1F300 && code <= 0x1F64F) || // Misc Symbols (emoji)
|
|
18
|
-
(code >= 0x20000 && code <= 0x3FFFD)) // SIP / TIP
|
|
19
|
-
return 2
|
|
20
|
-
return 1
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function visualWidth(s: string): number {
|
|
24
|
-
let w = 0; for (const c of s) w += charColumns(c); return w
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function visualPadEnd(s: string, cols: number): string {
|
|
28
|
-
const pad = cols - visualWidth(s)
|
|
29
|
-
return pad > 0 ? s + " ".repeat(pad) : s
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
|
|
33
|
-
export function truncateVisual(s: string, maxCols: number): string {
|
|
34
|
-
if (visualWidth(s) <= maxCols) return s
|
|
35
|
-
let result = "", w = 0
|
|
36
|
-
for (const c of s) {
|
|
37
|
-
const cw = charColumns(c)
|
|
38
|
-
if (w + cw > maxCols - 1) { result += "\u2026"; break }
|
|
39
|
-
result += c; w += cw
|
|
40
|
-
}
|
|
41
|
-
return result
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function progressBar(percent: number, width: number): string {
|
|
45
|
-
const clamped = Math.max(0, Math.min(100, percent))
|
|
46
|
-
const filled = Math.round((clamped / 100) * width)
|
|
47
|
-
const empty = Math.max(0, width - filled)
|
|
48
|
-
return "\u2588".repeat(filled) + "\u2591".repeat(empty)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function fmt(n: number): string {
|
|
52
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
|
|
53
|
-
if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
|
|
54
|
-
return n.toLocaleString("en-US")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function num(v: unknown): number {
|
|
58
|
-
return typeof v === "number" && Number.isFinite(v) ? v : 0
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function fmtCost(n: number, symbol = "$", rate = 1): string {
|
|
62
|
-
const v = n * rate
|
|
63
|
-
if (v >= 1) return symbol + v.toFixed(2)
|
|
64
|
-
if (v >= 0.01) return symbol + v.toFixed(3)
|
|
65
|
-
return symbol + v.toFixed(4)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
|
|
69
|
-
export function fmtCompact(n: number): string {
|
|
70
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
|
|
71
|
-
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
|
|
72
|
-
return String(Math.round(n))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
|
|
76
|
-
export function formatBalanceAmount(total: string): string {
|
|
77
|
-
const n = parseFloat(total)
|
|
78
|
-
if (!Number.isFinite(n)) return total
|
|
79
|
-
if (n === 0 || n >= 1) return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
80
|
-
return n.toLocaleString("en-US", { maximumFractionDigits: 6 })
|
|
81
|
-
}
|
|
1
|
+
/** CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
2
|
+
* string length (=1 per char), which breaks alignment with mixed text. */
|
|
3
|
+
|
|
4
|
+
export function charColumns(c: string): number {
|
|
5
|
+
const code = c.codePointAt(0) ?? 0
|
|
6
|
+
if (code < 0x20) return 0 // control
|
|
7
|
+
if (code < 0x7F) return 1 // ASCII
|
|
8
|
+
if (code < 0xA0) return 0 // C1 controls
|
|
9
|
+
// East-Asian wide / fullwidth ranges
|
|
10
|
+
if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
|
|
11
|
+
(code >= 0x2E80 && code <= 0xA4CF) || // CJK Radicals … Yi
|
|
12
|
+
(code >= 0xAC00 && code <= 0xD7A3) || // Hangul
|
|
13
|
+
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compat
|
|
14
|
+
(code >= 0xFE10 && code <= 0xFE6F) || // Vertical / Compat
|
|
15
|
+
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth
|
|
16
|
+
(code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth signs
|
|
17
|
+
(code >= 0x1F300 && code <= 0x1F64F) || // Misc Symbols (emoji)
|
|
18
|
+
(code >= 0x20000 && code <= 0x3FFFD)) // SIP / TIP
|
|
19
|
+
return 2
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function visualWidth(s: string): number {
|
|
24
|
+
let w = 0; for (const c of s) w += charColumns(c); return w
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function visualPadEnd(s: string, cols: number): string {
|
|
28
|
+
const pad = cols - visualWidth(s)
|
|
29
|
+
return pad > 0 ? s + " ".repeat(pad) : s
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
|
|
33
|
+
export function truncateVisual(s: string, maxCols: number): string {
|
|
34
|
+
if (visualWidth(s) <= maxCols) return s
|
|
35
|
+
let result = "", w = 0
|
|
36
|
+
for (const c of s) {
|
|
37
|
+
const cw = charColumns(c)
|
|
38
|
+
if (w + cw > maxCols - 1) { result += "\u2026"; break }
|
|
39
|
+
result += c; w += cw
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function progressBar(percent: number, width: number): string {
|
|
45
|
+
const clamped = Math.max(0, Math.min(100, percent))
|
|
46
|
+
const filled = Math.round((clamped / 100) * width)
|
|
47
|
+
const empty = Math.max(0, width - filled)
|
|
48
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fmt(n: number): string {
|
|
52
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
|
|
53
|
+
if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
|
|
54
|
+
return n.toLocaleString("en-US")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function num(v: unknown): number {
|
|
58
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function fmtCost(n: number, symbol = "$", rate = 1): string {
|
|
62
|
+
const v = n * rate
|
|
63
|
+
if (v >= 1) return symbol + v.toFixed(2)
|
|
64
|
+
if (v >= 0.01) return symbol + v.toFixed(3)
|
|
65
|
+
return symbol + v.toFixed(4)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
|
|
69
|
+
export function fmtCompact(n: number): string {
|
|
70
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
|
|
71
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
|
|
72
|
+
return String(Math.round(n))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
|
|
76
|
+
export function formatBalanceAmount(total: string): string {
|
|
77
|
+
const n = parseFloat(total)
|
|
78
|
+
if (!Number.isFinite(n)) return total
|
|
79
|
+
if (n === 0 || n >= 1) return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
80
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 })
|
|
81
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from "./color"
|
|
2
|
-
export * from "./currency"
|
|
3
|
-
export * from "./estimate"
|
|
4
|
-
export * from "./format"
|
|
5
|
-
export * from "./types"
|
|
1
|
+
export * from "./color"
|
|
2
|
+
export * from "./currency"
|
|
3
|
+
export * from "./estimate"
|
|
4
|
+
export * from "./format"
|
|
5
|
+
export * from "./types"
|
package/src/core/types.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token distribution breakdown for a session round.
|
|
3
|
-
* Pure data model shared by V1/V2 shells.
|
|
4
|
-
*/
|
|
5
|
-
export interface TokenDist {
|
|
6
|
-
system: number // UserMessage.system
|
|
7
|
-
user: number // user message text/file parts
|
|
8
|
-
agent: number // task tool input prompt/description (sub-agent delegation)
|
|
9
|
-
toolCall: number // ToolPart.input (actual tool params)
|
|
10
|
-
toolResult: number // ToolPart completed output / error
|
|
11
|
-
output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
|
|
12
|
-
reasoning: number // AssistantMessage.tokens.reasoning (API exact)
|
|
13
|
-
apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
|
|
14
|
-
apiInput: number // API exact total input context (input + cache read + cache write)
|
|
15
|
-
stepCost: number // last step-finish part cost (USD) in the current round
|
|
16
|
-
stepCount: number // step-finish parts count across the current round (parentID chain)
|
|
17
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Token distribution breakdown for a session round.
|
|
3
|
+
* Pure data model shared by V1/V2 shells.
|
|
4
|
+
*/
|
|
5
|
+
export interface TokenDist {
|
|
6
|
+
system: number // UserMessage.system
|
|
7
|
+
user: number // user message text/file parts
|
|
8
|
+
agent: number // task tool input prompt/description (sub-agent delegation)
|
|
9
|
+
toolCall: number // ToolPart.input (actual tool params)
|
|
10
|
+
toolResult: number // ToolPart completed output / error
|
|
11
|
+
output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
|
|
12
|
+
reasoning: number // AssistantMessage.tokens.reasoning (API exact)
|
|
13
|
+
apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
|
|
14
|
+
apiInput: number // API exact total input context (input + cache read + cache write)
|
|
15
|
+
stepCost: number // last step-finish part cost (USD) in the current round
|
|
16
|
+
stepCount: number // step-finish parts count across the current round (parentID chain)
|
|
17
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -493,6 +493,9 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
493
493
|
onSubmit={input.on_submit}
|
|
494
494
|
ref={input.ref}
|
|
495
495
|
hint={<BottomStatusBar api={api} signals={signals} sessionId={input.session_id} />}
|
|
496
|
+
// 接管 session_prompt 后需透传宿主的 session_prompt_right 插槽,
|
|
497
|
+
// 否则 oc-tps 等依赖该插槽的插件无法显示;无注册时 Slot 为 null。
|
|
498
|
+
right={<api.ui.Slot name="session_prompt_right" session_id={input.session_id} />}
|
|
496
499
|
/>
|
|
497
500
|
)
|
|
498
501
|
},
|
package/src/panel/panel-api.ts
CHANGED
|
@@ -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 }
|