Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ If you keep plugin config in `tui.json`, that works too:

The throughput section shows end-to-end output rate, not decode-only model TPS.

It is calculated as output tokens divided by total assistant message duration.
It is calculated as output tokens divided by the time between the first visible assistant text and message completion.

## Current Limits

Expand All @@ -57,4 +57,4 @@ bun run build

## Release

The standalone repo is intended to publish through GitHub Actions with npm trusted publishing.
The standalone repo is intended to publish through GitHub Actions with npm trusted publishing.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode-usage-dashboard",
"version": "1.0.2",
"version": "1.0.3",
"description": "TUI usage dashboard plugin for OpenCode",
"type": "module",
"license": "MIT",
Expand Down
135 changes: 105 additions & 30 deletions src/agg.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import type { Message, Part } from "@opencode-ai/sdk/v2"
import { dayKey, putMsg, putTool } from "./agg"
import { createAgg } from "./state"
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import type { Message, Part } from "@opencode-ai/sdk/v2";
import { dayKey, putMsg, putSpeed, putTool } from "./agg";
import { createAgg } from "./state";

function message(overrides: Partial<Message> = {}) {
return {
Expand All @@ -25,7 +25,7 @@ function message(overrides: Partial<Message> = {}) {
completed: Date.parse("2026-01-02T10:00:05.000Z"),
},
...overrides,
} as Message
} as Message;
}

function tool(status: "running" | "completed" | "error"): Part {
Expand All @@ -36,7 +36,7 @@ function tool(status: "running" | "completed" | "error"): Part {
sessionID: "session-1",
tool: "bash",
state: { status: "running" },
} as Part
} as Part;
}
return {
id: "tool-1",
Expand All @@ -50,38 +50,113 @@ function tool(status: "running" | "completed" | "error"): Part {
end: Date.parse("2026-01-02T10:00:02.000Z"),
},
},
} as Part
} as Part;
}

function text(
start: number,
overrides: Partial<Extract<Part, { type: "text" }>> = {},
): Part {
return {
id: `text-${start}`,
type: "text",
sessionID: "session-1",
messageID: "msg-1",
text: "hello",
time: { start },
...overrides,
} as Part;
}

describe("agg", () => {
it("counts a completed assistant message once", () => {
const agg = createAgg()
const msg = message()
const completed = (msg.time as { completed: number }).completed
const agg = createAgg();
const msg = message();
const completed = (msg.time as { completed: number }).completed;

assert.equal(putMsg(agg, msg), true);

const day = agg.by_s[msg.sessionID]![dayKey(completed)]!;
assert.equal(day.totals.msg, 1);
assert.equal(day.totals.cost, 1.25);
assert.equal(day.models["provider/model"]!.output, 20);
assert.equal(day.agents.build!.n, 1);
});

it("uses first visible text start for throughput", () => {
const agg = createAgg();
const msg = message({
time: {
created: Date.parse("2026-01-02T10:00:00.000Z"),
completed: Date.parse("2026-01-02T10:00:05.000Z"),
},
});
const completed = (msg.time as { completed: number }).completed;

assert.equal(
putSpeed(agg, msg, [
text(Date.parse("2026-01-02T10:00:03.000Z")),
text(Date.parse("2026-01-02T10:00:04.000Z")),
]),
true,
);

const day = agg.by_s[msg.sessionID]![dayKey(completed)]!;
assert.equal(day.speed["provider/model"]!.out, 20);
assert.equal(day.speed["provider/model"]!.ms, 2000);
assert.equal(day.speed["provider/model"]!.n, 1);
});

it("ignores synthetic and ignored text when computing throughput", () => {
const agg = createAgg();
const msg = message({
time: {
created: Date.parse("2026-01-02T10:00:00.000Z"),
completed: Date.parse("2026-01-02T10:00:05.000Z"),
},
});
const completed = (msg.time as { completed: number }).completed;

assert.equal(
putSpeed(agg, msg, [
text(Date.parse("2026-01-02T10:00:01.000Z"), { synthetic: true }),
text(Date.parse("2026-01-02T10:00:02.000Z"), { ignored: true }),
text(Date.parse("2026-01-02T10:00:04.000Z")),
]),
true,
);

const day = agg.by_s[msg.sessionID]![dayKey(completed)]!;
assert.equal(day.speed["provider/model"]!.ms, 1000);
});

assert.equal(putMsg(agg, msg), true)
it("skips throughput when there is no visible text start", () => {
const agg = createAgg();
const msg = message();

const day = agg.by_s[msg.sessionID]![dayKey(completed)]!
assert.equal(day.totals.msg, 1)
assert.equal(day.totals.cost, 1.25)
assert.equal(day.models["provider/model"]!.output, 20)
assert.equal(day.agents.build!.n, 1)
})
assert.equal(
putSpeed(agg, msg, [
text(Date.parse("2026-01-02T10:00:01.000Z"), { synthetic: true }),
]),
false,
);
assert.equal(agg.by_s[msg.sessionID], undefined);
});

it("ignores non-terminal tool updates", () => {
const agg = createAgg()
const agg = createAgg();
const completed = tool("completed") as Part & {
state: { status: "completed"; time: { start: number; end: number } }
}
state: { status: "completed"; time: { start: number; end: number } };
};

assert.equal(putTool(agg, tool("running")), false)
assert.equal(agg.by_s["session-1"], undefined)
assert.equal(putTool(agg, tool("running")), false);
assert.equal(agg.by_s["session-1"], undefined);

assert.equal(putTool(agg, completed), true)
assert.equal(putTool(agg, completed), true);

const day = agg.by_s["session-1"]![dayKey(completed.state.time.end)]!
assert.equal(day.totals.tool, 1)
assert.equal(day.tools.bash!.n, 1)
assert.equal(day.tools.bash!.err, 0)
})
})
const day = agg.by_s["session-1"]![dayKey(completed.state.time.end)]!;
assert.equal(day.totals.tool, 1);
assert.equal(day.tools.bash!.n, 1);
assert.equal(day.tools.bash!.err, 0);
});
});
Loading
Loading