opencode-total-session-cost 1.0.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 +44 -2
- package/cost.ts +136 -0
- package/index.ts +1 -1
- package/package.json +19 -1
- package/tui.tsx +85 -75
- 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
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Opencode Total Session Cost Tracker 💰
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
6
|
+
[](https://github.com/StayPirate/opencode-total-session-cost/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/StayPirate/opencode-total-session-cost)
|
|
8
|
+
[](https://github.com/StayPirate/opencode-total-session-cost/stargazers)
|
|
9
|
+
|
|
3
10
|
A real-time, lightweight plugin for [Opencode](https://opencode.im) that tracks and displays the **actual cumulative cost** of your active session, including all background tasks, sub-agents, and child sessions.
|
|
4
11
|
|
|
5
12
|
## Why this plugin?
|
|
@@ -12,19 +19,54 @@ Without tracking these sub-sessions, you might see a main session cost of a few
|
|
|
12
19
|
## Features
|
|
13
20
|
- **Sidebar-Independent**: The cost indicator stays visible even when the Opencode sidebar is closed.
|
|
14
21
|
- **Recursive Sub-Agent Tracking**: Automatically detects and sums up the costs of all child tasks spawned during your session.
|
|
15
|
-
- **Detailed Cost Breakdown**: Offers a `/total_cost` slash command to show a detailed popup separating your active session cost from child sub-agent costs.
|
|
22
|
+
- **Detailed Cost Breakdown**: Offers a `/total_cost` slash command to show a detailed popup separating your active session cost from child task and sub-agent costs.
|
|
23
|
+
- **Provider & Model Breakdown**: Displays the exact costs accumulated per model across all session hierarchy levels.
|
|
24
|
+
- **Mouse Click Interaction**: Left-clicking on the cost bar in the prompt header right panel triggers the same detailed breakdown popup.
|
|
16
25
|
|
|
17
26
|
## Installation
|
|
18
|
-
|
|
27
|
+
Install it globally with:
|
|
19
28
|
```bash
|
|
20
29
|
opencode plugin opencode-total-session-cost -g
|
|
21
30
|
```
|
|
22
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
|
+
|
|
23
58
|
## How it works
|
|
24
59
|
The plugin runs inside Opencode's TUI framework, recursively fetching session data:
|
|
25
60
|
1. It reads the core cost of your active session.
|
|
26
61
|
2. It recursively queries all child sub-sessions spawned by sub-agents.
|
|
27
62
|
3. It displays the combined sum dynamically in the TUI.
|
|
28
63
|
|
|
64
|
+
## Development
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run typecheck
|
|
68
|
+
npm test
|
|
69
|
+
```
|
|
70
|
+
|
|
29
71
|
## License
|
|
30
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.
|
|
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,61 +1,102 @@
|
|
|
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";
|
|
11
|
+
|
|
12
|
+
export const id = "opencode-total-session-cost";
|
|
13
|
+
|
|
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
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { role: message.role };
|
|
24
|
+
};
|
|
3
25
|
|
|
4
|
-
|
|
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 [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
5
39
|
|
|
6
40
|
export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
7
|
-
|
|
41
|
+
const showCostBreakdown = async (sessionID: string) => {
|
|
42
|
+
const breakdown = await collectCosts(sessionID, createDeps(api));
|
|
43
|
+
|
|
44
|
+
api.ui.toast({
|
|
45
|
+
title: "Session Costs Breakdown",
|
|
46
|
+
message: formatBreakdown(breakdown),
|
|
47
|
+
variant: "success",
|
|
48
|
+
duration: 10000
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
8
52
|
api.slots?.register({
|
|
9
53
|
slots: {
|
|
10
|
-
|
|
11
|
-
|
|
54
|
+
session_prompt_right: (
|
|
55
|
+
ctx: Readonly<TuiSlotContext>,
|
|
56
|
+
props: TuiHostSlotMap["session_prompt_right"]
|
|
57
|
+
) => {
|
|
12
58
|
const [total, setTotal] = createSignal<number>(0);
|
|
13
|
-
|
|
14
|
-
const calculateCost = async (sessionID: string): Promise<number> => {
|
|
15
|
-
let totalCost = 0;
|
|
16
|
-
|
|
17
|
-
const sessionObj = api.state.session.get(sessionID);
|
|
18
|
-
if (sessionObj && typeof sessionObj.cost === "number") {
|
|
19
|
-
totalCost += sessionObj.cost;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const childrenRes = await api.client.session.children({ sessionID });
|
|
24
|
-
if (childrenRes.data) {
|
|
25
|
-
for (const child of childrenRes.data) {
|
|
26
|
-
totalCost += await calculateCost(child.id);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
} catch (err) {}
|
|
30
|
-
|
|
31
|
-
return totalCost;
|
|
32
|
-
};
|
|
59
|
+
let requestId = 0;
|
|
33
60
|
|
|
34
61
|
const update = async () => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
setTotal(res);
|
|
39
|
-
} else {
|
|
62
|
+
const currentRequest = ++requestId;
|
|
63
|
+
const sessionID = props.session_id;
|
|
64
|
+
if (!sessionID) {
|
|
40
65
|
setTotal(0);
|
|
66
|
+
return;
|
|
41
67
|
}
|
|
68
|
+
|
|
69
|
+
const breakdown = await collectCosts(sessionID, createDeps(api));
|
|
70
|
+
if (currentRequest !== requestId) return;
|
|
71
|
+
|
|
72
|
+
setTotal(breakdown.total);
|
|
42
73
|
};
|
|
43
74
|
|
|
44
|
-
const unsubMsgUpdated = api.event.on("message.updated", update);
|
|
45
|
-
const unsubMsgRemoved = api.event.on("message.removed", update);
|
|
46
|
-
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);
|
|
47
80
|
|
|
48
81
|
onCleanup(() => {
|
|
49
82
|
unsubMsgUpdated();
|
|
50
83
|
unsubMsgRemoved();
|
|
84
|
+
unsubSessionUpdated();
|
|
85
|
+
unsubSessionIdle();
|
|
51
86
|
clearInterval(interval);
|
|
52
87
|
});
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
update();
|
|
89
|
+
void update();
|
|
56
90
|
|
|
57
91
|
return (
|
|
58
|
-
<text
|
|
92
|
+
<text
|
|
93
|
+
fg={ctx.theme.current.textMuted}
|
|
94
|
+
onMouseUp={(event) => {
|
|
95
|
+
if (event.button === 0) {
|
|
96
|
+
void showCostBreakdown(props.session_id);
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
59
100
|
{" "}[ ${total().toFixed(2)} ]
|
|
60
101
|
</text>
|
|
61
102
|
);
|
|
@@ -63,7 +104,6 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
63
104
|
}
|
|
64
105
|
});
|
|
65
106
|
|
|
66
|
-
// Register slash command /total_cost (and alias /costs)
|
|
67
107
|
api.command?.register(() => [
|
|
68
108
|
{
|
|
69
109
|
title: "Total Session Cost",
|
|
@@ -75,9 +115,12 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
75
115
|
aliases: ["costs"]
|
|
76
116
|
},
|
|
77
117
|
onSelect: async () => {
|
|
78
|
-
const currentSessionID =
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
124
|
|
|
82
125
|
if (!currentSessionID) {
|
|
83
126
|
api.ui.toast({
|
|
@@ -89,50 +132,17 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
|
|
|
89
132
|
return;
|
|
90
133
|
}
|
|
91
134
|
|
|
92
|
-
|
|
93
|
-
let childrenCost = 0;
|
|
94
|
-
|
|
95
|
-
const calculateCostRecursive = async (sessionID: string): Promise<void> => {
|
|
96
|
-
const sessionObj = api.state.session.get(sessionID);
|
|
97
|
-
if (sessionObj && typeof sessionObj.cost === "number") {
|
|
98
|
-
if (sessionID === currentSessionID) {
|
|
99
|
-
parentCost += sessionObj.cost;
|
|
100
|
-
} else {
|
|
101
|
-
childrenCost += sessionObj.cost;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const childrenRes = await api.client.session.children({ sessionID });
|
|
107
|
-
if (childrenRes.data) {
|
|
108
|
-
for (const child of childrenRes.data) {
|
|
109
|
-
await calculateCostRecursive(child.id);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch (err) {}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
await calculateCostRecursive(currentSessionID);
|
|
116
|
-
const total = parentCost + childrenCost;
|
|
117
|
-
|
|
118
|
-
api.ui.toast({
|
|
119
|
-
title: "Session Costs Breakdown",
|
|
120
|
-
message: `Active: $${parentCost.toFixed(2)} | Children: $${childrenCost.toFixed(2)} | Total: $${total.toFixed(2)}`,
|
|
121
|
-
variant: "success",
|
|
122
|
-
duration: 6000
|
|
123
|
-
});
|
|
135
|
+
await showCostBreakdown(currentSessionID);
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
]);
|
|
127
139
|
};
|
|
128
140
|
|
|
129
|
-
// Export named constant for loaders looking for `export const tui = ...`
|
|
130
141
|
export const tui = SessionCostPlugin;
|
|
131
142
|
|
|
132
|
-
// Export default module configuration
|
|
133
143
|
const pluginModule: TuiPluginModule = {
|
|
134
144
|
id,
|
|
135
145
|
tui: SessionCostPlugin
|
|
136
146
|
};
|
|
137
147
|
|
|
138
|
-
export default pluginModule;
|
|
148
|
+
export default pluginModule;
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: Continuous Integration
|
|
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
|
-
}
|