@streetturtle/opencode-session-tokens 1.0.0
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 +20 -0
- package/dist/tui.tsx +119 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @streetturtle/opencode-session-tokens
|
|
2
|
+
|
|
3
|
+
OpenCode sidebar plugin for session token usage in the current session.
|
|
4
|
+
|
|
5
|
+
- Collapsed: `▶ Session Tokens <total>`
|
|
6
|
+
- Expanded: `▼ Session Tokens <total>` with per-model breakdown
|
|
7
|
+
- Total uses session token counters when available
|
|
8
|
+
- Breakdown aggregates assistant response spend (`input + output + reasoning + cache.write`)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
opencode plugin @streetturtle/opencode-session-tokens
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Global install:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
opencode plugin --global @streetturtle/opencode-session-tokens
|
|
20
|
+
```
|
package/dist/tui.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { createMemo, createSignal, For, Show } from "solid-js"
|
|
3
|
+
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
4
|
+
|
|
5
|
+
const MAX_MODEL_ROWS = 10
|
|
6
|
+
const INT_FORMATTER = new Intl.NumberFormat("en-US")
|
|
7
|
+
|
|
8
|
+
function safeNumber(value: unknown): number {
|
|
9
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function spentTokenCount(tokens: any): number {
|
|
13
|
+
const input = safeNumber(tokens?.input)
|
|
14
|
+
const output = safeNumber(tokens?.output)
|
|
15
|
+
const reasoning = safeNumber(tokens?.reasoning)
|
|
16
|
+
const cacheWrite = safeNumber(tokens?.cache?.write)
|
|
17
|
+
return input + output + reasoning + cacheWrite
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatInt(value: number): string {
|
|
21
|
+
return INT_FORMATTER.format(Math.max(0, Math.round(value)))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shortModelLabel(label: string): string {
|
|
25
|
+
if (label.length <= 28) return label
|
|
26
|
+
return `${label.slice(0, 25)}...`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function View(props: { api: TuiPluginApi; sessionID: string }) {
|
|
30
|
+
const [open, setOpen] = createSignal(false)
|
|
31
|
+
const theme = () => props.api.theme.current
|
|
32
|
+
const messages = createMemo(() => props.api.state.session.messages(props.sessionID) as any[])
|
|
33
|
+
const session = createMemo(() => props.api.state.session.get(props.sessionID) as any)
|
|
34
|
+
|
|
35
|
+
const data = createMemo(() => {
|
|
36
|
+
const totals = new Map<string, number>()
|
|
37
|
+
let breakdownTotal = 0
|
|
38
|
+
const seen = new Set<string>()
|
|
39
|
+
|
|
40
|
+
for (const message of messages()) {
|
|
41
|
+
const role = message?.role ?? message?.info?.role
|
|
42
|
+
if (role !== "assistant") continue
|
|
43
|
+
|
|
44
|
+
const messageID = message?.id
|
|
45
|
+
if (typeof messageID === "string" && seen.has(messageID)) continue
|
|
46
|
+
if (typeof messageID === "string") seen.add(messageID)
|
|
47
|
+
|
|
48
|
+
const count = spentTokenCount(message?.tokens)
|
|
49
|
+
if (count <= 0) continue
|
|
50
|
+
|
|
51
|
+
const modelID = message?.modelID ?? message?.info?.modelID ?? "unknown"
|
|
52
|
+
|
|
53
|
+
breakdownTotal += count
|
|
54
|
+
totals.set(modelID, (totals.get(modelID) ?? 0) + count)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const perModel = [...totals.entries()]
|
|
58
|
+
.map(([model, tokens]) => ({ model, tokens }))
|
|
59
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
60
|
+
|
|
61
|
+
const sessionTotal = spentTokenCount(session()?.tokens)
|
|
62
|
+
const total = breakdownTotal > 0 ? breakdownTotal : sessionTotal
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
total,
|
|
66
|
+
perModel,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const show = createMemo(() => data().total > 0)
|
|
71
|
+
const canExpand = createMemo(() => data().perModel.length > 0)
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Show when={show()}>
|
|
75
|
+
<box>
|
|
76
|
+
<box flexDirection="row" gap={1} onMouseDown={() => canExpand() && setOpen((x) => !x)}>
|
|
77
|
+
<Show when={canExpand()}>
|
|
78
|
+
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
|
79
|
+
</Show>
|
|
80
|
+
<text fg={theme().text}>
|
|
81
|
+
<b>Session Tokens</b>
|
|
82
|
+
</text>
|
|
83
|
+
<text fg={theme().textMuted}>{formatInt(data().total)}</text>
|
|
84
|
+
</box>
|
|
85
|
+
|
|
86
|
+
<Show when={canExpand() && open()}>
|
|
87
|
+
<For each={data().perModel.slice(0, MAX_MODEL_ROWS)}>{(row) => (
|
|
88
|
+
<box flexDirection="row">
|
|
89
|
+
<text fg={theme().textMuted}>{shortModelLabel(row.model)}</text>
|
|
90
|
+
<box flexGrow={1} />
|
|
91
|
+
<text fg={theme().textMuted}>{formatInt(row.tokens)}</text>
|
|
92
|
+
</box>
|
|
93
|
+
)}</For>
|
|
94
|
+
<Show when={data().perModel.length > MAX_MODEL_ROWS}>
|
|
95
|
+
<text fg={theme().textMuted}>+{data().perModel.length - MAX_MODEL_ROWS} more</text>
|
|
96
|
+
</Show>
|
|
97
|
+
</Show>
|
|
98
|
+
</box>
|
|
99
|
+
</Show>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tui: TuiPlugin = async (api) => {
|
|
104
|
+
api.slots.register({
|
|
105
|
+
order: 120,
|
|
106
|
+
slots: {
|
|
107
|
+
sidebar_content(_ctx, props) {
|
|
108
|
+
return <View api={api} sessionID={props.session_id} />
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
115
|
+
id: "streetturtle.session-tokens",
|
|
116
|
+
tui,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default plugin
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streetturtle/opencode-session-tokens",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode TUI sidebar plugin for session token totals and per-model breakdown",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/streetturtle/opencode-better-sidebar",
|
|
8
|
+
"directory": "plugins/session-tokens"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/streetturtle/opencode-better-sidebar/tree/main/plugins/session-tokens",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/streetturtle/opencode-better-sidebar/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./tui": "./dist/tui.tsx"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "rm -rf dist && mkdir -p dist && cp index.ts dist/tui.tsx",
|
|
24
|
+
"pack:dry": "npm pack --dry-run",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"opencode",
|
|
29
|
+
"opencode-plugin",
|
|
30
|
+
"sidebar",
|
|
31
|
+
"tokens",
|
|
32
|
+
"session-tokens"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"opencode": ">=1.4.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opencode-ai/plugin": "^1.4.3",
|
|
40
|
+
"@opentui/core": "^0.2.2",
|
|
41
|
+
"@opentui/solid": "^0.2.2",
|
|
42
|
+
"solid-js": "^1.9.12"
|
|
43
|
+
}
|
|
44
|
+
}
|