opencode-total-session-cost 1.1.1 → 1.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/LICENSE +21 -0
- package/README.md +34 -1
- package/cost.ts +136 -0
- package/index.ts +1 -1
- package/package.json +19 -1
- package/tui.tsx +69 -131
- package/.github/workflows/ci.yml +0 -27
- package/.github/workflows/publish.yml +0 -30
- package/opencode.jsonc +0 -6
- package/tsconfig.json +0 -15
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 crazybyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -24,16 +24,49 @@ Without tracking these sub-sessions, you might see a main session cost of a few
|
|
|
24
24
|
- **Mouse Click Interaction**: Left-clicking on the cost bar in the prompt header right panel triggers the same detailed breakdown popup.
|
|
25
25
|
|
|
26
26
|
## Installation
|
|
27
|
-
|
|
27
|
+
Install it globally with:
|
|
28
28
|
```bash
|
|
29
29
|
opencode plugin opencode-total-session-cost -g
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Cost categories
|
|
33
|
+
The breakdown separates costs into three buckets:
|
|
34
|
+
- **Session**: the cost of the active/root session.
|
|
35
|
+
- **Task**: child sessions spawned through the Task tool by the `explore` and `general` agents.
|
|
36
|
+
- **Sub-agent**: any other child session.
|
|
37
|
+
|
|
38
|
+
Costs that are not present in the session messages (for example archived or compacted
|
|
39
|
+
messages) are still attributed to the session's configured provider/model, so the totals
|
|
40
|
+
always match the real spending reported by Opencode.
|
|
41
|
+
|
|
42
|
+
Example `/total_cost` output:
|
|
43
|
+
```
|
|
44
|
+
By session
|
|
45
|
+
Session: $0.12
|
|
46
|
+
Task: $0.34
|
|
47
|
+
Sub-agent: $0.08
|
|
48
|
+
---------------
|
|
49
|
+
Total: $0.54
|
|
50
|
+
|
|
51
|
+
By provider/model
|
|
52
|
+
anthropic/claude-sonnet-4: $0.40
|
|
53
|
+
openai/gpt-5: $0.14
|
|
54
|
+
------------------------------
|
|
55
|
+
Total: $0.54
|
|
56
|
+
```
|
|
57
|
+
|
|
32
58
|
## How it works
|
|
33
59
|
The plugin runs inside Opencode's TUI framework, recursively fetching session data:
|
|
34
60
|
1. It reads the core cost of your active session.
|
|
35
61
|
2. It recursively queries all child sub-sessions spawned by sub-agents.
|
|
36
62
|
3. It displays the combined sum dynamically in the TUI.
|
|
37
63
|
|
|
64
|
+
## Development
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run typecheck
|
|
68
|
+
npm test
|
|
69
|
+
```
|
|
70
|
+
|
|
38
71
|
## License
|
|
39
72
|
MIT
|
package/cost.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export type SessionLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
cost?: number;
|
|
4
|
+
agent?: string;
|
|
5
|
+
model?: {
|
|
6
|
+
providerID?: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type MessageLike = {
|
|
12
|
+
role?: string;
|
|
13
|
+
cost?: number;
|
|
14
|
+
providerID?: string;
|
|
15
|
+
modelID?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CostDeps = {
|
|
19
|
+
getSession: (sessionID: string) => SessionLike | undefined;
|
|
20
|
+
getChildren: (sessionID: string) => Promise<readonly SessionLike[]>;
|
|
21
|
+
getMessages: (sessionID: string) => readonly MessageLike[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SessionCosts = {
|
|
25
|
+
parent: number;
|
|
26
|
+
task: number;
|
|
27
|
+
subagent: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CostBreakdown = {
|
|
31
|
+
sessions: SessionCosts;
|
|
32
|
+
models: Record<string, number>;
|
|
33
|
+
total: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Agents that OpenCode spawns through the Task tool. Their sessions are
|
|
38
|
+
* reported under the "Task" category in the breakdown.
|
|
39
|
+
*/
|
|
40
|
+
export const TASK_AGENTS: ReadonlySet<string> = new Set(["explore", "general"]);
|
|
41
|
+
|
|
42
|
+
const EPSILON = 0.0001;
|
|
43
|
+
|
|
44
|
+
export async function collectCosts(rootSessionID: string, deps: CostDeps): Promise<CostBreakdown> {
|
|
45
|
+
const sessions: SessionCosts = { parent: 0, task: 0, subagent: 0 };
|
|
46
|
+
const models: Record<string, number> = {};
|
|
47
|
+
const visited = new Set<string>();
|
|
48
|
+
|
|
49
|
+
const addModelCost = (providerID: string, modelID: string, cost: number) => {
|
|
50
|
+
const key = `${providerID}/${modelID}`;
|
|
51
|
+
models[key] = (models[key] ?? 0) + cost;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const walk = async (sessionID: string, fallback?: SessionLike): Promise<void> => {
|
|
55
|
+
if (visited.has(sessionID)) return;
|
|
56
|
+
visited.add(sessionID);
|
|
57
|
+
|
|
58
|
+
const session = deps.getSession(sessionID) ?? fallback;
|
|
59
|
+
const sessionCost = session && typeof session.cost === "number" ? session.cost : 0;
|
|
60
|
+
|
|
61
|
+
if (sessionID === rootSessionID) {
|
|
62
|
+
sessions.parent += sessionCost;
|
|
63
|
+
} else if (session?.agent && TASK_AGENTS.has(session.agent)) {
|
|
64
|
+
sessions.task += sessionCost;
|
|
65
|
+
} else {
|
|
66
|
+
sessions.subagent += sessionCost;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let attributedCost = 0;
|
|
70
|
+
for (const message of deps.getMessages(sessionID)) {
|
|
71
|
+
if (message.role !== "assistant") continue;
|
|
72
|
+
const cost = message.cost;
|
|
73
|
+
if (typeof cost !== "number" || cost <= 0) continue;
|
|
74
|
+
addModelCost(
|
|
75
|
+
message.providerID ?? session?.model?.providerID ?? "unknown",
|
|
76
|
+
message.modelID ?? session?.model?.id ?? "unknown",
|
|
77
|
+
cost
|
|
78
|
+
);
|
|
79
|
+
attributedCost += cost;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remainder = sessionCost - attributedCost;
|
|
83
|
+
if (remainder > EPSILON) {
|
|
84
|
+
addModelCost(
|
|
85
|
+
session?.model?.providerID ?? "unknown",
|
|
86
|
+
session?.model?.id ?? "unknown",
|
|
87
|
+
remainder
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const child of await deps.getChildren(sessionID)) {
|
|
92
|
+
await walk(child.id, child);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await walk(rootSessionID);
|
|
97
|
+
|
|
98
|
+
const total = sessions.parent + sessions.task + sessions.subagent;
|
|
99
|
+
return { sessions, models, total };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatSessionSection(sessions: SessionCosts): string {
|
|
103
|
+
const total = sessions.parent + sessions.task + sessions.subagent;
|
|
104
|
+
return [
|
|
105
|
+
"By session",
|
|
106
|
+
`Session: $${sessions.parent.toFixed(2)}`,
|
|
107
|
+
`Task: $${sessions.task.toFixed(2)}`,
|
|
108
|
+
`Sub-agent: $${sessions.subagent.toFixed(2)}`,
|
|
109
|
+
"---------------",
|
|
110
|
+
`Total: $${total.toFixed(2)}`,
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatModelSection(models: Record<string, number>, total: number): string {
|
|
115
|
+
const sorted = Object.entries(models).sort((a, b) => b[1] - a[1]);
|
|
116
|
+
|
|
117
|
+
let labelWidth = "Total:".length;
|
|
118
|
+
for (const [key] of sorted) {
|
|
119
|
+
labelWidth = Math.max(labelWidth, key.length + 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lines = sorted.map(
|
|
123
|
+
([key, cost]) => `${`${key}:`.padEnd(labelWidth + 1, " ")}$${cost.toFixed(2)}`
|
|
124
|
+
);
|
|
125
|
+
const separator = "-".repeat(labelWidth + 7);
|
|
126
|
+
const totalLine = `${"Total:".padEnd(labelWidth + 1, " ")}$${total.toFixed(2)}`;
|
|
127
|
+
|
|
128
|
+
return ["By provider/model", ...lines, separator, totalLine].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatBreakdown(breakdown: CostBreakdown): string {
|
|
132
|
+
return `${formatSessionSection(breakdown.sessions)}\n\n${formatModelSection(
|
|
133
|
+
breakdown.models,
|
|
134
|
+
breakdown.total
|
|
135
|
+
)}`;
|
|
136
|
+
}
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-total-session-cost",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Real-time cumulative cost tracker for Opencode sessions, aggregating costs from parent sessions, archived messages, and all child sub-agent tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
".": "./index.ts",
|
|
9
9
|
"./tui": "./tui.tsx"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"cost.ts",
|
|
13
|
+
"index.ts",
|
|
14
|
+
"tui.tsx",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"overrides": {
|
|
27
|
+
"@babel/core": "^7.29.7"
|
|
28
|
+
},
|
|
11
29
|
"keywords": [
|
|
12
30
|
"opencode-plugin",
|
|
13
31
|
"opencode",
|
package/tui.tsx
CHANGED
|
@@ -1,161 +1,99 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
TuiHostSlotMap,
|
|
3
|
+
TuiPlugin,
|
|
4
|
+
TuiPluginApi,
|
|
5
|
+
TuiPluginModule,
|
|
6
|
+
TuiSlotContext
|
|
7
|
+
} from "@opencode-ai/plugin/tui";
|
|
8
|
+
import type { Message } from "@opencode-ai/sdk/v2";
|
|
2
9
|
import { createSignal, onCleanup } from "solid-js";
|
|
10
|
+
import { collectCosts, formatBreakdown, type CostDeps, type MessageLike } from "./cost.ts";
|
|
3
11
|
|
|
4
|
-
export const id = "opencode-cost
|
|
12
|
+
export const id = "opencode-total-session-cost";
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const addModelCost = (providerID: string, modelID: string, cost: number) => {
|
|
15
|
-
const key = `${providerID}/${modelID}`;
|
|
16
|
-
modelCosts[key] = (modelCosts[key] || 0) + cost;
|
|
14
|
+
const toMessageLike = (message: Message): MessageLike => {
|
|
15
|
+
if (message.role === "assistant") {
|
|
16
|
+
return {
|
|
17
|
+
role: message.role,
|
|
18
|
+
cost: message.cost,
|
|
19
|
+
providerID: message.providerID,
|
|
20
|
+
modelID: message.modelID
|
|
17
21
|
};
|
|
22
|
+
}
|
|
23
|
+
return { role: message.role };
|
|
24
|
+
};
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
subagentCost += sessionCost;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let messagesAttributedCost = 0;
|
|
34
|
-
try {
|
|
35
|
-
const msgRes = await api.client.session.messages({ sessionID: currID });
|
|
36
|
-
if (msgRes.data) {
|
|
37
|
-
for (const item of msgRes.data) {
|
|
38
|
-
const msg = item.info as any;
|
|
39
|
-
if (msg && msg.role === "assistant" && typeof msg.cost === "number" && msg.cost > 0) {
|
|
40
|
-
const provider = msg.providerID || (sessionObj?.model?.providerID) || "unknown";
|
|
41
|
-
const model = msg.modelID || (sessionObj?.model?.id) || "unknown";
|
|
42
|
-
addModelCost(provider, model, msg.cost);
|
|
43
|
-
messagesAttributedCost += msg.cost;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {}
|
|
48
|
-
|
|
49
|
-
const remainder = sessionCost - messagesAttributedCost;
|
|
50
|
-
if (remainder > 0.0001) {
|
|
51
|
-
const provider = (sessionObj?.model?.providerID) || "unknown";
|
|
52
|
-
const model = (sessionObj?.model?.id) || "unknown";
|
|
53
|
-
addModelCost(provider, model, remainder);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const childrenRes = await api.client.session.children({ sessionID: currID });
|
|
58
|
-
if (childrenRes.data) {
|
|
59
|
-
for (const child of childrenRes.data) {
|
|
60
|
-
await calculateCostRecursive(child.id, child);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch (err) {}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
await calculateCostRecursive(sessionID);
|
|
67
|
-
const total = parentCost + taskCost + subagentCost;
|
|
68
|
-
|
|
69
|
-
// Format Session breakdown
|
|
70
|
-
const sessionSection = `By session\nSession: $${parentCost.toFixed(2)}\nTask: $${taskCost.toFixed(2)}\nSub-agent: $${subagentCost.toFixed(2)}\n---------------\nTotal: $${total.toFixed(2)}`;
|
|
71
|
-
|
|
72
|
-
// Format Model breakdown
|
|
73
|
-
let maxLabelLength = 6; // Length of "Total:" is 6
|
|
74
|
-
for (const key of Object.keys(modelCosts)) {
|
|
75
|
-
const label = `${key}:`;
|
|
76
|
-
if (label.length > maxLabelLength) {
|
|
77
|
-
maxLabelLength = label.length;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const sortedModels = Object.entries(modelCosts).sort((a, b) => b[1] - a[1]);
|
|
82
|
-
const modelLines: string[] = [];
|
|
83
|
-
for (const [key, cost] of sortedModels) {
|
|
84
|
-
const label = `${key}:`;
|
|
85
|
-
const paddedLabel = label.padEnd(maxLabelLength + 1, " ");
|
|
86
|
-
modelLines.push(`${paddedLabel}$${cost.toFixed(2)}`);
|
|
26
|
+
const createDeps = (api: TuiPluginApi): CostDeps => ({
|
|
27
|
+
getSession: (sessionID) => api.state.session.get(sessionID),
|
|
28
|
+
getMessages: (sessionID) => api.state.session.messages(sessionID).map(toMessageLike),
|
|
29
|
+
getChildren: async (sessionID) => {
|
|
30
|
+
try {
|
|
31
|
+
const res = await api.client.session.children({ sessionID });
|
|
32
|
+
return res.data ?? [];
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`[${id}] failed to load children of session ${sessionID}`, error);
|
|
35
|
+
return [];
|
|
87
36
|
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
88
39
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const modelSection = `By provider/model\n${modelLines.join("\n")}\n${modelSeparator}\n${modelTotalLine}`;
|
|
40
|
+
export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
41
|
+
const showCostBreakdown = async (sessionID: string) => {
|
|
42
|
+
const breakdown = await collectCosts(sessionID, createDeps(api));
|
|
94
43
|
|
|
95
44
|
api.ui.toast({
|
|
96
45
|
title: "Session Costs Breakdown",
|
|
97
|
-
message:
|
|
46
|
+
message: formatBreakdown(breakdown),
|
|
98
47
|
variant: "success",
|
|
99
48
|
duration: 10000
|
|
100
49
|
});
|
|
101
50
|
};
|
|
102
51
|
|
|
103
|
-
// Register TUI slots with reactive signals defined inside the renderers
|
|
104
52
|
api.slots?.register({
|
|
105
53
|
slots: {
|
|
106
|
-
|
|
107
|
-
|
|
54
|
+
session_prompt_right: (
|
|
55
|
+
ctx: Readonly<TuiSlotContext>,
|
|
56
|
+
props: TuiHostSlotMap["session_prompt_right"]
|
|
57
|
+
) => {
|
|
108
58
|
const [total, setTotal] = createSignal<number>(0);
|
|
109
|
-
|
|
110
|
-
const calculateCost = async (sessionID: string): Promise<number> => {
|
|
111
|
-
let totalCost = 0;
|
|
112
|
-
|
|
113
|
-
const sessionObj = api.state.session.get(sessionID);
|
|
114
|
-
if (sessionObj && typeof sessionObj.cost === "number") {
|
|
115
|
-
totalCost += sessionObj.cost;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const childrenRes = await api.client.session.children({ sessionID });
|
|
120
|
-
if (childrenRes.data) {
|
|
121
|
-
for (const child of childrenRes.data) {
|
|
122
|
-
totalCost += await calculateCost(child.id);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} catch (err) {}
|
|
126
|
-
|
|
127
|
-
return totalCost;
|
|
128
|
-
};
|
|
59
|
+
let requestId = 0;
|
|
129
60
|
|
|
130
61
|
const update = async () => {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
setTotal(res);
|
|
135
|
-
} else {
|
|
62
|
+
const currentRequest = ++requestId;
|
|
63
|
+
const sessionID = props.session_id;
|
|
64
|
+
if (!sessionID) {
|
|
136
65
|
setTotal(0);
|
|
66
|
+
return;
|
|
137
67
|
}
|
|
68
|
+
|
|
69
|
+
const breakdown = await collectCosts(sessionID, createDeps(api));
|
|
70
|
+
if (currentRequest !== requestId) return;
|
|
71
|
+
|
|
72
|
+
setTotal(breakdown.total);
|
|
138
73
|
};
|
|
139
74
|
|
|
140
|
-
const unsubMsgUpdated = api.event.on("message.updated", update);
|
|
141
|
-
const unsubMsgRemoved = api.event.on("message.removed", update);
|
|
142
|
-
const
|
|
75
|
+
const unsubMsgUpdated = api.event.on("message.updated", () => void update());
|
|
76
|
+
const unsubMsgRemoved = api.event.on("message.removed", () => void update());
|
|
77
|
+
const unsubSessionUpdated = api.event.on("session.updated", () => void update());
|
|
78
|
+
const unsubSessionIdle = api.event.on("session.idle", () => void update());
|
|
79
|
+
const interval = setInterval(() => void update(), 3000);
|
|
143
80
|
|
|
144
81
|
onCleanup(() => {
|
|
145
82
|
unsubMsgUpdated();
|
|
146
83
|
unsubMsgRemoved();
|
|
84
|
+
unsubSessionUpdated();
|
|
85
|
+
unsubSessionIdle();
|
|
147
86
|
clearInterval(interval);
|
|
148
87
|
});
|
|
149
88
|
|
|
150
|
-
|
|
151
|
-
update();
|
|
89
|
+
void update();
|
|
152
90
|
|
|
153
91
|
return (
|
|
154
92
|
<text
|
|
155
|
-
fg=
|
|
156
|
-
onMouseUp={(
|
|
157
|
-
if (
|
|
158
|
-
showCostBreakdown(props.session_id);
|
|
93
|
+
fg={ctx.theme.current.textMuted}
|
|
94
|
+
onMouseUp={(event) => {
|
|
95
|
+
if (event.button === 0) {
|
|
96
|
+
void showCostBreakdown(props.session_id);
|
|
159
97
|
}
|
|
160
98
|
}}
|
|
161
99
|
>
|
|
@@ -166,7 +104,6 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
166
104
|
}
|
|
167
105
|
});
|
|
168
106
|
|
|
169
|
-
// Register slash command /total_cost (and alias /costs)
|
|
170
107
|
api.command?.register(() => [
|
|
171
108
|
{
|
|
172
109
|
title: "Total Session Cost",
|
|
@@ -178,9 +115,12 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
178
115
|
aliases: ["costs"]
|
|
179
116
|
},
|
|
180
117
|
onSelect: async () => {
|
|
181
|
-
const currentSessionID =
|
|
182
|
-
|
|
183
|
-
|
|
118
|
+
const currentSessionID =
|
|
119
|
+
api.route.current &&
|
|
120
|
+
api.route.current.name === "session" &&
|
|
121
|
+
typeof api.route.current.params?.sessionID === "string"
|
|
122
|
+
? api.route.current.params.sessionID
|
|
123
|
+
: null;
|
|
184
124
|
|
|
185
125
|
if (!currentSessionID) {
|
|
186
126
|
api.ui.toast({
|
|
@@ -198,13 +138,11 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
198
138
|
]);
|
|
199
139
|
};
|
|
200
140
|
|
|
201
|
-
// Export named constant for loaders looking for `export const tui = ...`
|
|
202
141
|
export const tui = SessionCostPlugin;
|
|
203
142
|
|
|
204
|
-
// Export default module configuration
|
|
205
143
|
const pluginModule: TuiPluginModule = {
|
|
206
144
|
id,
|
|
207
145
|
tui: SessionCostPlugin
|
|
208
146
|
};
|
|
209
147
|
|
|
210
|
-
export default pluginModule;
|
|
148
|
+
export default pluginModule;
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ main ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
typecheck:
|
|
11
|
-
name: TypeScript Typecheck
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout Code
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Bun
|
|
19
|
-
uses: oven-sh/setup-bun@v2
|
|
20
|
-
with:
|
|
21
|
-
bun-version: latest
|
|
22
|
-
|
|
23
|
-
- name: Install Dependencies
|
|
24
|
-
run: bun install --frozen-lockfile
|
|
25
|
-
|
|
26
|
-
- name: Run Typecheck
|
|
27
|
-
run: bun x tsc --noEmit
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
name: Publish Package
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
permissions:
|
|
13
|
-
contents: read
|
|
14
|
-
id-token: write
|
|
15
|
-
steps:
|
|
16
|
-
- name: Checkout Code
|
|
17
|
-
uses: actions/checkout@v6
|
|
18
|
-
|
|
19
|
-
- name: Setup Node.js
|
|
20
|
-
uses: actions/setup-node@v6
|
|
21
|
-
with:
|
|
22
|
-
node-version: 24
|
|
23
|
-
registry-url: 'https://registry.npmjs.org'
|
|
24
|
-
package-manager-cache: false
|
|
25
|
-
|
|
26
|
-
- name: Install Dependencies
|
|
27
|
-
run: npm ci
|
|
28
|
-
|
|
29
|
-
- name: Publish Package
|
|
30
|
-
run: npm publish --access public
|
package/opencode.jsonc
DELETED
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"jsx": "preserve",
|
|
7
|
-
"jsxImportSource": "@opentui/solid",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["./index.ts", "./tui.tsx"]
|
|
15
|
-
}
|