@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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/tui.tsx +119 -0
  3. 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
+ }