Feature: Display Session Cost in Web UI#416
Conversation
Greptile SummaryThis PR adds end-to-end support for displaying the cumulative session cost in the web UI, covering backend persistence (persisting The implementation is clean and well-tested. One functional bug was found:
A minor edge case also exists in Important Files Changed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/web/src/components/sidebar/metadata-section.tsx
Line: 103-107
Comment:
**Dollar sign displayed twice**
The `<span className="w-4 text-center">$</span>` element renders a `$` character as a visual icon, but `formatSessionCost(totalCost)` (in `session-cost.ts` line 13) already returns a string prefixed with `$` — e.g. `"$0.0168"`. The result is the cost rendering as `$ OpenCode cost $0.0168`.
Either remove the `$` icon span and rely on `formatSessionCost`'s output, or keep the icon span and strip the `$` from the formatted value:
```suggestion
{typeof totalCost === "number" && totalCost > 0 && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="w-4 text-center">$</span>
<span>Session cost: {totalCost >= 1 ? totalCost.toFixed(2) : totalCost.toFixed(4)}</span>
</div>
)}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/web/src/lib/session-cost.ts
Line: 11-14
Comment:
**No handling for costs just above zero but effectively zero**
`formatSessionCost` uses a hard threshold of `>= 1` to switch between 2 and 4 decimal places. For very small costs (e.g. `$0.00001`), `toFixed(4)` rounds to `"$0.0000"`, misleadingly suggesting the session was free. Consider using `toPrecision` for sub-cent amounts:
```suggestion
export function formatSessionCost(cost: number): string {
if (cost >= 1) return `$${cost.toFixed(2)}`;
if (cost >= 0.01) return `$${cost.toFixed(4)}`;
return `$${cost.toPrecision(2)}`;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Display Session Cost in Web UI" | Re-trigger Greptile |
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Architectural feedback: persist cost on
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds session cost tracking across the stack: DB schema + migration, repository method to accumulate costs, sandbox event handling to record step_finish costs, durable object and socket propagation of totalCost, UI formatting/display, and unit tests. Changes
Sequence DiagramsequenceDiagram
actor Sandbox
participant EventProc as Event Processor
participant Repo as Repository
participant DB as Session DB
participant Socket
participant Hook as useSessionSocket
participant UI as UI Components
Sandbox->>EventProc: step_finish { cost: N }
EventProc->>Repo: addSessionCost(N)
Repo->>DB: UPDATE session SET total_cost = total_cost + N
DB-->>Repo: OK
EventProc->>Socket: broadcast({ type: "sandbox_event", event })
Socket->>Hook: receive sandbox_event
Hook->>Hook: if finite cost → totalCost = (prev.totalCost ?? 0) + N
Hook->>UI: set sessionState { totalCost: M }
UI->>UI: formatSessionCost(M)
UI->>UI: render "Session cost: $X"
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/web/src/lib/session-cost.test.ts (1)
4-16: Consider adding boundary assertions for threshold stability.Please add exact-threshold checks for Line 2/Line 3 branch boundaries (
cost === 1andcost === 0.01) to lock behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/lib/session-cost.test.ts` around lines 4 - 16, Add two exact-threshold tests for formatSessionCost to lock branch behavior: assert formatSessionCost(1) returns "$1.00" (explicit two-decimal dollar formatting) and assert formatSessionCost(0.01) returns "$0.0100" (explicit four-decimal sub-dollar formatting). Place these assertions alongside the existing tests in the describe("formatSessionCost") block so the cost === 1 and cost === 0.01 boundaries are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/control-plane/src/session/sandbox-events.test.ts`:
- Around line 108-123: The guard that checks event.cost currently uses typeof
(allowing NaN/Infinity) so update the check in the processSandboxEvent handler
to use Number.isFinite(event.cost) before calling repository.addSessionCost;
then add test cases to sandbox-events.test.ts that call
h.processor.processSandboxEvent with event.cost = NaN and event.cost = Infinity
and assert h.repository.addSessionCost is not called and broadcast still occurs
(reference the processSandboxEvent method, the addSessionCost mock on
h.repository, and the existing step_finish test to mirror structure).
---
Nitpick comments:
In `@packages/web/src/lib/session-cost.test.ts`:
- Around line 4-16: Add two exact-threshold tests for formatSessionCost to lock
branch behavior: assert formatSessionCost(1) returns "$1.00" (explicit
two-decimal dollar formatting) and assert formatSessionCost(0.01) returns
"$0.0100" (explicit four-decimal sub-dollar formatting). Place these assertions
alongside the existing tests in the describe("formatSessionCost") block so the
cost === 1 and cost === 0.01 boundaries are covered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0b9b4101-78ef-40df-8adb-13ef412c65df
📒 Files selected for processing (13)
packages/control-plane/src/session/durable-object.tspackages/control-plane/src/session/repository.test.tspackages/control-plane/src/session/repository.tspackages/control-plane/src/session/sandbox-events.test.tspackages/control-plane/src/session/sandbox-events.tspackages/control-plane/src/session/schema.tspackages/control-plane/src/session/types.tspackages/shared/src/types/index.tspackages/web/src/components/session-right-sidebar.tsxpackages/web/src/components/sidebar/metadata-section.tsxpackages/web/src/hooks/use-session-socket.tspackages/web/src/lib/session-cost.test.tspackages/web/src/lib/session-cost.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/web/src/hooks/use-session-socket.ts (1)
247-252:⚠️ Potential issue | 🟡 MinorSanitize subscribed
totalCostbefore storing it in state.Line 251 currently uses
data.state.totalCost ?? 0, which does not rejectNaN/Infinity. If that slips through, downstream formatting can render invalid values and future increments will propagate bad state.Suggested fix
setSessionState({ ...data.state, // Backward-compatible default for older sessions that may omit this. isProcessing: data.state.isProcessing ?? false, - totalCost: data.state.totalCost ?? 0, + totalCost: + typeof data.state.totalCost === "number" && Number.isFinite(data.state.totalCost) + ? data.state.totalCost + : 0, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/hooks/use-session-socket.ts` around lines 247 - 252, The code stores unvalidated totalCost from the socket payload into state; replace the direct usage of data.state.totalCost in the setSessionState call with a sanitized finite-number check (e.g., compute sanitizedTotalCost = Number.isFinite(Number(data.state.totalCost)) ? Number(data.state.totalCost) : 0) and use sanitizedTotalCost when calling setSessionState so NaN/Infinity/invalid values are coerced to 0 before being saved (refer to setSessionState and data.state.totalCost in the diff).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/web/src/hooks/use-session-socket.ts`:
- Around line 247-252: The code stores unvalidated totalCost from the socket
payload into state; replace the direct usage of data.state.totalCost in the
setSessionState call with a sanitized finite-number check (e.g., compute
sanitizedTotalCost = Number.isFinite(Number(data.state.totalCost)) ?
Number(data.state.totalCost) : 0) and use sanitizedTotalCost when calling
setSessionState so NaN/Infinity/invalid values are coerced to 0 before being
saved (refer to setSessionState and data.state.totalCost in the diff).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8671ef4c-1176-49e1-8f39-3d6278c41c0a
📒 Files selected for processing (12)
packages/control-plane/src/session/durable-object.tspackages/control-plane/src/session/repository.test.tspackages/control-plane/src/session/repository.tspackages/control-plane/src/session/sandbox-events.test.tspackages/control-plane/src/session/sandbox-events.tspackages/control-plane/src/session/schema.tspackages/control-plane/src/session/types.tspackages/shared/src/types/index.tspackages/web/src/components/session-right-sidebar.tsxpackages/web/src/components/sidebar/metadata-section.tsxpackages/web/src/hooks/use-session-socket.tspackages/web/src/lib/session-cost.test.ts
✅ Files skipped from review due to trivial changes (5)
- packages/control-plane/src/session/schema.ts
- packages/web/src/lib/session-cost.test.ts
- packages/control-plane/src/session/repository.test.ts
- packages/control-plane/src/session/sandbox-events.test.ts
- packages/control-plane/src/session/durable-object.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/web/src/components/session-right-sidebar.tsx
- packages/shared/src/types/index.ts
- packages/control-plane/src/session/types.ts
`Number.isFinite` alone does not narrow `number | undefined` to `number`. Adding `typeof event.cost === "number"` gives tsc the type-guard it needs.
## Summary Follow-up to #416 (session cost display). Addresses three review findings: - **Make `total_cost` non-optional on `SessionRow`** — the DB column is `NOT NULL DEFAULT 0`, so the TypeScript type should reflect that. Prevents `undefined` from leaking through to consumers. - **Update `updated_at` in `addSessionCost`** — every other session mutation sets `updated_at`; cost increments were the only exception. This ensures recency queries and staleness checks account for cost updates. - **Reject negative costs** — adds `event.cost > 0` guard in both backend (`sandbox-events.ts`) and frontend (`use-session-socket.ts`) to prevent malformed events from decrementing the running total. ## Test plan - [x] All 938 control-plane unit tests pass - [x] TypeScript typecheck passes for control-plane and web - [x] New test case for negative cost rejection - [x] Updated existing `addSessionCost` test to verify `updated_at` is set - [x] Updated 7 test fixtures to include required `total_cost: 0` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Session cost tracking now ignores zero or negative cost values and only applies positive costs. * Session cost updates now include timestamp information for more accurate recording. * **Chores** * Test fixtures updated across the codebase to reflect the expanded session data shape. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Adding a feature to display the session cost in the web interface

Summary by CodeRabbit
New Features
Chores
Tests