This repository was archived by the owner on Jun 8, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Phase 2: Per-session billing mode + two-ledger UI (FEA-1434) #254
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3ebc393
FEA-1434: Detect billing mode and split UI into two ledgers
ea40777
FEA-1434: Address codex review — UI parity, spawn coverage, sync migr…
6ea51df
FEA-1434: Wire FEA-1439 into contract migration TODO
6cd1453
FEA-1434: Bump version (missed in 6ea51df contract comment edit)
ee4f7e4
FEA-1434: Address codex review round 2 — spawn coverage + writeback race
43d86c8
FEA-1434: Address Claude review — settings wiring + writeback clobber…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,27 @@ import { formatDateTime, formatDuration, truncate, fmtCost } from "../lib/format | |
| import { effectiveSessionStatus, isSessionAwaitingInput } from "../lib/types"; | ||
| import type { Session, DashboardEvent } from "../lib/types"; | ||
|
|
||
| // CLOSEDLOOP FEA-1434: subscription-covered billing modes. Mirrors the | ||
| // canonical enum in src/shared/billing-mode.ts on the desktop side. Inlined | ||
| // here because the agent-monitor client is bundled separately from the | ||
| // desktop main TypeScript tree. | ||
| const SUBSCRIPTION_BILLING_MODES = new Set<string>([ | ||
| "claude_pro", | ||
| "claude_max", | ||
| "codex_chatgpt_pro", | ||
| "cursor_pro", | ||
| "copilot_seat", | ||
| "opencode", | ||
| ]); | ||
|
|
||
| function isApiMetered(billingMode: string | null | undefined): boolean { | ||
| return billingMode === "api"; | ||
| } | ||
|
|
||
| function isSubscriptionCovered(billingMode: string | null | undefined): boolean { | ||
| return !!billingMode && SUBSCRIPTION_BILLING_MODES.has(billingMode); | ||
| } | ||
|
|
||
| const PAGE_SIZE = 10; | ||
| export function Sessions() { | ||
| const navigate = useNavigate(); | ||
|
|
@@ -169,6 +190,23 @@ export function Sessions() { | |
| const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); | ||
| const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected); | ||
|
|
||
| // CLOSEDLOOP FEA-1434: split the visible page of sessions into two ledgers. | ||
| // The page is paginated server-side so these totals are page-local; full | ||
| // workspace rollups belong on the Dashboard. Keeping the math local avoids | ||
| // a separate API round-trip just for the totals. | ||
| const ledgerTotals = sessions.reduce( | ||
| (acc, s) => { | ||
| const cost = s.cost ?? 0; | ||
| if (isApiMetered(s.billing_mode)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This rollup drops anything that isn't 'api' or a subscription mode, so an 'unknown' session lands in neither card. But down at line 462 that same row still renders its cost as a bare dollar amount, identical to an api row. 'unknown' is the schema default for every pre-migration row and for any spawn where detection fell through (no API key env, no creds file), so you'll routinely have rows showing '$3.00' in the column that aren't in the Metered API spend total. The two cards won't add up to what the user sees in the table. Either fold unknown into the api bucket or render it muted so it's clear it's uncounted. |
||
| acc.api += cost; | ||
| } else if (isSubscriptionCovered(s.billing_mode)) { | ||
| acc.subscription += cost; | ||
| } | ||
| return acc; | ||
| }, | ||
| { api: 0, subscription: 0 }, | ||
| ); | ||
|
|
||
| return ( | ||
| <div className="animate-fade-in"> | ||
| <div className="flex flex-wrap items-center justify-between gap-3 mb-8"> | ||
|
|
@@ -300,6 +338,33 @@ export function Sessions() { | |
| /> | ||
| ) : ( | ||
| <> | ||
| {/* FEA-1434: two-ledger rollup. Subscription-covered cost never | ||
| sums into the headline — it's surfaced as an "equivalent" for | ||
| transparency only. */} | ||
| <div className="mb-4 grid grid-cols-1 sm:grid-cols-2 gap-3"> | ||
| <div className="card px-4 py-3"> | ||
| <div className="text-[11px] font-semibold text-gray-500 uppercase tracking-wider"> | ||
| Metered API spend | ||
| </div> | ||
| <div className="mt-1 text-lg font-mono text-gray-100"> | ||
| {fmtCost(ledgerTotals.api)} | ||
| </div> | ||
| <div className="text-[11px] text-gray-500"> | ||
| Sessions billed per-token via API keys (this page) | ||
| </div> | ||
| </div> | ||
| <div className="card px-4 py-3"> | ||
| <div className="text-[11px] font-semibold text-gray-500 uppercase tracking-wider"> | ||
| Subscription-covered | ||
| </div> | ||
| <div className="mt-1 text-lg font-mono text-gray-300"> | ||
| {fmtCost(ledgerTotals.subscription)} equiv. | ||
| </div> | ||
| <div className="text-[11px] text-gray-500"> | ||
| Pro / Max / per-seat — token-cost equivalent only (this page) | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className="card overflow-x-auto"> | ||
| <table className="w-full min-w-[800px]"> | ||
| <thead> | ||
|
|
@@ -374,7 +439,28 @@ export function Sessions() { | |
| {session.agent_count ?? "-"} | ||
| </td> | ||
| <td className="px-5 py-4 text-sm text-gray-400 font-mono"> | ||
| {session.cost != null && session.cost > 0 ? fmtCost(session.cost) : "-"} | ||
| {/* FEA-1434: render API-metered cost as a dollar amount; | ||
| subscription-covered cost is muted and prefixed | ||
| "Covered ·" so the user can see at a glance which | ||
| ledger a row contributes to. */} | ||
| {(() => { | ||
| const cost = session.cost; | ||
| const billing = session.billing_mode; | ||
| if (cost == null || cost <= 0) { | ||
| return "-"; | ||
| } | ||
| if (isApiMetered(billing)) { | ||
| return fmtCost(cost); | ||
| } | ||
| if (isSubscriptionCovered(billing)) { | ||
| return ( | ||
| <span className="text-gray-500"> | ||
| Covered · {fmtCost(cost)} equiv. | ||
| </span> | ||
| ); | ||
| } | ||
| return fmtCost(cost); | ||
| })()} | ||
| </td> | ||
| <td | ||
| className="px-5 py-4 text-[11px] text-gray-500 font-mono" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hand copy of SUBSCRIPTION_MODES in billing-mode.ts with nothing tying them together. Add a 9th subscription mode there and the importers/detector will stamp it but this UI silently buckets it as uncounted, and no build gate catches the drift. The separate-bundle reason is fair, but worth at least a test that asserts the two sets match.