opencode-usage-total 0.1.1 → 0.1.2
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/index.js +238 -191
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,21 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// usage-total-tui.tsx
|
|
6
6
|
import { createRoot, createSignal } from "solid-js";
|
|
7
|
+
|
|
8
|
+
// package.json
|
|
9
|
+
var version = "0.1.2";
|
|
10
|
+
|
|
11
|
+
// usage-total-tui.tsx
|
|
7
12
|
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
8
13
|
var DEFAULT_AGENT = "primary";
|
|
9
14
|
var UNKNOWN_ID = "?";
|
|
10
15
|
function resolveRouteSessionID(api) {
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const current = api.route.current;
|
|
17
|
+
if (current.name === "session") {
|
|
18
|
+
const sid = current.params?.sessionID;
|
|
19
|
+
return typeof sid === "string" ? sid : void 0;
|
|
20
|
+
}
|
|
21
|
+
return void 0;
|
|
13
22
|
}
|
|
14
23
|
function modelTokens(m) {
|
|
15
24
|
return m.tokensInput + m.tokensOutput + m.tokensReasoning;
|
|
@@ -18,6 +27,9 @@ function safeNum(value) {
|
|
|
18
27
|
const n = typeof value === "number" ? value : Number(value);
|
|
19
28
|
return Number.isFinite(n) ? n : 0;
|
|
20
29
|
}
|
|
30
|
+
function roundCost(n) {
|
|
31
|
+
return safeNum(Number(n.toFixed(6)));
|
|
32
|
+
}
|
|
21
33
|
function fmtTokens(n) {
|
|
22
34
|
if (!Number.isFinite(n) || n === 0) return "0";
|
|
23
35
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
@@ -29,209 +41,244 @@ function fmtCost(n) {
|
|
|
29
41
|
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
30
42
|
return `$${n.toFixed(2)}`;
|
|
31
43
|
}
|
|
44
|
+
var initialized = false;
|
|
32
45
|
var tui = async (api) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setExpanded(next);
|
|
51
|
-
api.kv.set(EXPANDED_KV_KEY, next);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
bindings: [{ key: "alt+m", cmd: TOGGLE_CMD }]
|
|
56
|
-
}) : void 0;
|
|
57
|
-
const loadedSessions = /* @__PURE__ */ new Set();
|
|
58
|
-
function kvKey(sessionID) {
|
|
59
|
-
return `usage-total:models:${sessionID}`;
|
|
46
|
+
if (initialized) {
|
|
47
|
+
api.ui?.toast?.({
|
|
48
|
+
message: "usage-total TUI already loaded; skipping re-init",
|
|
49
|
+
variant: "warning"
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
initialized = true;
|
|
54
|
+
api.ui?.toast?.({ message: "usage-total TUI loaded", variant: "info" });
|
|
55
|
+
let unsub;
|
|
56
|
+
let keymapDispose;
|
|
57
|
+
let solidDispose;
|
|
58
|
+
let flushPending;
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
try {
|
|
61
|
+
flushPending?.();
|
|
62
|
+
} catch {
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
api.kv.set(kvKey(sessionID), models);
|
|
65
|
-
}
|
|
64
|
+
try {
|
|
65
|
+
unsub?.();
|
|
66
|
+
} catch {
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
try {
|
|
69
|
+
keymapDispose?.();
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
solidDispose?.();
|
|
74
|
+
} catch {
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
initialized = false;
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
if (api.lifecycle?.onDispose) {
|
|
80
|
+
api.lifecycle.onDispose(cleanup);
|
|
81
|
+
}
|
|
82
|
+
createRoot((dispose) => {
|
|
83
|
+
solidDispose = dispose;
|
|
84
|
+
const [modelState, setModelState] = createSignal({});
|
|
85
|
+
const EXPANDED_KV_KEY = "usage-total.sidebar.expanded";
|
|
86
|
+
const [expanded, setExpanded] = createSignal(
|
|
87
|
+
api.kv?.get?.(EXPANDED_KV_KEY, true) !== false
|
|
81
88
|
);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
tokensOutput: safeOutput,
|
|
102
|
-
tokensReasoning: safeReasoning,
|
|
103
|
-
messageCount: 1
|
|
104
|
-
});
|
|
89
|
+
const TOGGLE_CMD = "usage-total.toggle-section";
|
|
90
|
+
keymapDispose = api.keymap?.registerLayer ? api.keymap.registerLayer({
|
|
91
|
+
commands: [
|
|
92
|
+
{
|
|
93
|
+
name: TOGGLE_CMD,
|
|
94
|
+
title: "Usage: Toggle models section",
|
|
95
|
+
description: "Collapse or expand the models list in the sidebar",
|
|
96
|
+
run: () => {
|
|
97
|
+
const next = !expanded();
|
|
98
|
+
setExpanded(next);
|
|
99
|
+
api.kv?.set?.(EXPANDED_KV_KEY, next);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
bindings: [{ key: "alt+m", cmd: TOGGLE_CMD }]
|
|
104
|
+
}) : void 0;
|
|
105
|
+
const loadedSessions = /* @__PURE__ */ new Set();
|
|
106
|
+
function kvKey(sessionID) {
|
|
107
|
+
return `usage-total:models:${sessionID}`;
|
|
105
108
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
109
|
+
const SAVE_DEBOUNCE_MS = 500;
|
|
110
|
+
const dirtySessions = /* @__PURE__ */ new Set();
|
|
111
|
+
let saveTimer;
|
|
112
|
+
function flushDirtySessions() {
|
|
113
|
+
if (saveTimer) {
|
|
114
|
+
clearTimeout(saveTimer);
|
|
115
|
+
saveTimer = void 0;
|
|
116
|
+
}
|
|
117
|
+
for (const sid of dirtySessions) {
|
|
118
|
+
const models = modelState()[sid];
|
|
119
|
+
if (models && models.length > 0) {
|
|
120
|
+
api.kv?.set?.(kvKey(sid), models);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
dirtySessions.clear();
|
|
120
124
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
flushPending = flushDirtySessions;
|
|
126
|
+
function scheduleSave(sessionID) {
|
|
127
|
+
dirtySessions.add(sessionID);
|
|
128
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
129
|
+
saveTimer = setTimeout(flushDirtySessions, SAVE_DEBOUNCE_MS);
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let provider;
|
|
133
|
-
let model;
|
|
134
|
-
let agent;
|
|
135
|
-
let cost = 0;
|
|
136
|
-
let tokens = {};
|
|
137
|
-
if (info.role === "user") {
|
|
138
|
-
const mdl = info.model;
|
|
139
|
-
provider = (mdl == null ? void 0 : mdl.providerID) ?? UNKNOWN_ID;
|
|
140
|
-
model = (mdl == null ? void 0 : mdl.modelID) ?? UNKNOWN_ID;
|
|
141
|
-
agent = info.agent ?? DEFAULT_AGENT;
|
|
142
|
-
} else if (info.role === "assistant") {
|
|
143
|
-
provider = info.providerID ?? UNKNOWN_ID;
|
|
144
|
-
model = info.modelID ?? UNKNOWN_ID;
|
|
145
|
-
agent = info.agent ?? info.mode ?? DEFAULT_AGENT;
|
|
146
|
-
cost = safeNum(info.cost);
|
|
147
|
-
tokens = {
|
|
148
|
-
input: safeNum((_b = info.tokens) == null ? void 0 : _b.input),
|
|
149
|
-
output: safeNum((_c = info.tokens) == null ? void 0 : _c.output),
|
|
150
|
-
reasoning: safeNum((_d = info.tokens) == null ? void 0 : _d.reasoning)
|
|
151
|
-
};
|
|
152
|
-
} else {
|
|
153
|
-
return;
|
|
131
|
+
function loadSession(sessionID) {
|
|
132
|
+
if (loadedSessions.has(sessionID)) return;
|
|
133
|
+
loadedSessions.add(sessionID);
|
|
134
|
+
const saved = api.kv?.get?.(kvKey(sessionID));
|
|
135
|
+
if (saved && saved.length > 0) {
|
|
136
|
+
setModelState((current) => ({ ...current, [sessionID]: saved }));
|
|
137
|
+
}
|
|
154
138
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
139
|
+
function upsertModel(sessionID, entry, cost, tokens) {
|
|
140
|
+
const dedupeKey = `${entry.provider}/${entry.model}/${entry.agent}`;
|
|
141
|
+
const current = modelState();
|
|
142
|
+
const sessionModels = [...current[sessionID] ?? []];
|
|
143
|
+
const existingIdx = sessionModels.findIndex(
|
|
144
|
+
(m) => `${m.provider}/${m.model}/${m.agent}` === dedupeKey
|
|
145
|
+
);
|
|
146
|
+
const safeCost = safeNum(cost);
|
|
147
|
+
const safeInput = safeNum(tokens.input);
|
|
148
|
+
const safeOutput = safeNum(tokens.output);
|
|
149
|
+
const safeReasoning = safeNum(tokens.reasoning);
|
|
150
|
+
if (existingIdx >= 0) {
|
|
151
|
+
const existing = sessionModels[existingIdx];
|
|
152
|
+
sessionModels[existingIdx] = {
|
|
153
|
+
...existing,
|
|
154
|
+
cost: roundCost(existing.cost + safeCost),
|
|
155
|
+
tokensInput: safeNum(existing.tokensInput + safeInput),
|
|
156
|
+
tokensOutput: safeNum(existing.tokensOutput + safeOutput),
|
|
157
|
+
tokensReasoning: safeNum(existing.tokensReasoning + safeReasoning),
|
|
158
|
+
messageCount: existing.messageCount + 1
|
|
159
|
+
};
|
|
160
|
+
} else {
|
|
161
|
+
sessionModels.push({
|
|
162
|
+
...entry,
|
|
163
|
+
cost: safeCost,
|
|
164
|
+
tokensInput: safeInput,
|
|
165
|
+
tokensOutput: safeOutput,
|
|
166
|
+
tokensReasoning: safeReasoning,
|
|
167
|
+
messageCount: 1
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
setModelState({
|
|
171
|
+
...current,
|
|
172
|
+
[sessionID]: sessionModels
|
|
173
|
+
});
|
|
174
|
+
scheduleSave(sessionID);
|
|
175
|
+
return existingIdx < 0;
|
|
176
|
+
}
|
|
177
|
+
function trackModel(eventSessionID, entry, cost, tokens) {
|
|
178
|
+
const isNew = upsertModel(eventSessionID, entry, cost, tokens);
|
|
179
|
+
if (isNew) {
|
|
180
|
+
api.ui?.toast?.({
|
|
181
|
+
message: `${entry.agent}: ${entry.model}`,
|
|
182
|
+
variant: "success"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const session = api.state?.session?.get?.(eventSessionID);
|
|
186
|
+
if (session?.parentID && session.parentID !== eventSessionID) {
|
|
187
|
+
upsertModel(session.parentID, entry, cost, tokens);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
unsub = api.event?.on?.("message.updated", (event) => {
|
|
191
|
+
const info = event?.properties?.info;
|
|
192
|
+
if (!info) return;
|
|
193
|
+
const eventSessionID = info.sessionID;
|
|
194
|
+
if (!eventSessionID) return;
|
|
195
|
+
let provider;
|
|
196
|
+
let model;
|
|
197
|
+
let agent;
|
|
198
|
+
let cost = 0;
|
|
199
|
+
let tokens = {};
|
|
200
|
+
if (info.role === "user") {
|
|
201
|
+
const mdl = info.model;
|
|
202
|
+
provider = mdl?.providerID ?? UNKNOWN_ID;
|
|
203
|
+
model = mdl?.modelID ?? UNKNOWN_ID;
|
|
204
|
+
agent = info.agent ?? DEFAULT_AGENT;
|
|
205
|
+
} else if (info.role === "assistant") {
|
|
206
|
+
provider = info.providerID ?? UNKNOWN_ID;
|
|
207
|
+
model = info.modelID ?? UNKNOWN_ID;
|
|
208
|
+
agent = info.agent ?? info.mode ?? DEFAULT_AGENT;
|
|
209
|
+
cost = safeNum(info.cost);
|
|
210
|
+
tokens = {
|
|
211
|
+
input: safeNum(info.tokens?.input),
|
|
212
|
+
output: safeNum(info.tokens?.output),
|
|
213
|
+
reasoning: safeNum(info.tokens?.reasoning)
|
|
214
|
+
};
|
|
215
|
+
} else {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
trackModel(eventSessionID, { provider, model, agent }, cost, tokens);
|
|
219
|
+
});
|
|
220
|
+
api.slots?.register?.({
|
|
221
|
+
order: 200,
|
|
222
|
+
slots: {
|
|
223
|
+
sidebar_content(ctx, props) {
|
|
224
|
+
const sessionID = props.session_id ?? resolveRouteSessionID(api) ?? "";
|
|
225
|
+
if (sessionID) loadSession(sessionID);
|
|
226
|
+
const models = sessionID ? modelState()[sessionID] ?? [] : [];
|
|
227
|
+
if (!sessionID || models.length === 0) {
|
|
228
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
229
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.text, children: "\u{1F9E0} Models" }),
|
|
230
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: sessionID ? "waiting for messages..." : "open a session to track models" })
|
|
231
|
+
] });
|
|
232
|
+
}
|
|
233
|
+
const totalCost = models.reduce((sum, m) => sum + m.cost, 0);
|
|
234
|
+
const totalTokens = models.reduce(
|
|
235
|
+
(sum, m) => sum + modelTokens(m),
|
|
236
|
+
0
|
|
176
237
|
);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
children: [
|
|
189
|
-
/* @__PURE__ */ jsxs(
|
|
190
|
-
"box",
|
|
191
|
-
{
|
|
192
|
-
flexDirection: "row",
|
|
193
|
-
justifyContent: "space-between",
|
|
194
|
-
children: [
|
|
195
|
-
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
196
|
-
/* @__PURE__ */ jsxs(
|
|
197
|
-
"text",
|
|
198
|
-
{
|
|
199
|
-
fg: ctx.theme.current.text,
|
|
200
|
-
bold: true,
|
|
201
|
-
children: [
|
|
202
|
-
expanded() ? "\u25BC" : "\u25B6",
|
|
203
|
-
" \u{1F9E0} Models"
|
|
204
|
-
]
|
|
205
|
-
}
|
|
206
|
-
),
|
|
207
|
-
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: " 0.1.0" })
|
|
238
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
239
|
+
/* @__PURE__ */ jsxs(
|
|
240
|
+
"box",
|
|
241
|
+
{
|
|
242
|
+
flexDirection: "row",
|
|
243
|
+
justifyContent: "space-between",
|
|
244
|
+
children: [
|
|
245
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
246
|
+
/* @__PURE__ */ jsxs("text", { fg: ctx.theme.current.text, children: [
|
|
247
|
+
expanded() ? "\u25BC" : "\u25B6",
|
|
248
|
+
" \u{1F9E0} Models"
|
|
208
249
|
] }),
|
|
209
|
-
/* @__PURE__ */
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
] },
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
/* @__PURE__ */ jsxs("text", { fg: ctx.theme.current.textMuted, children: [
|
|
251
|
+
" ",
|
|
252
|
+
version
|
|
253
|
+
] })
|
|
254
|
+
] }),
|
|
255
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.text, children: totalCost > 0 ? `${fmtCost(totalCost)} \xB7 ${fmtTokens(totalTokens)}` : fmtTokens(totalTokens) })
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
),
|
|
259
|
+
expanded() && models.map((m) => /* @__PURE__ */ jsxs("box", { flexDirection: "column", children: [
|
|
260
|
+
/* @__PURE__ */ jsxs("text", { fg: ctx.theme.current.text, children: [
|
|
261
|
+
m.agent,
|
|
262
|
+
":"
|
|
263
|
+
] }),
|
|
264
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", justifyContent: "space-between", children: [
|
|
265
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.text, children: " " + m.model }),
|
|
266
|
+
/* @__PURE__ */ jsx("text", { fg: ctx.theme.current.textMuted, children: m.cost > 0 ? `${fmtCost(m.cost)}${modelTokens(m) > 0 ? ` \xB7 ${fmtTokens(modelTokens(m))}` : ""}` : modelTokens(m) > 0 ? fmtTokens(modelTokens(m)) : "-" })
|
|
267
|
+
] })
|
|
268
|
+
] }))
|
|
269
|
+
] });
|
|
270
|
+
}
|
|
226
271
|
}
|
|
227
|
-
}
|
|
272
|
+
});
|
|
228
273
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
274
|
+
} catch (err) {
|
|
275
|
+
cleanup();
|
|
276
|
+
api.ui?.toast?.({
|
|
277
|
+
message: "usage-total TUI failed to initialize",
|
|
278
|
+
variant: "error"
|
|
233
279
|
});
|
|
234
|
-
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
235
282
|
};
|
|
236
283
|
var usage_total_tui_default = { id: "usage-total", tui };
|
|
237
284
|
export {
|