Merge pull request #12303 from Kilo-Org/fix/cli-sidebar-usage

fix(cli): improve sidebar usage display
This commit is contained in:
Jean du Plessis
2026-07-17 11:06:53 +02:00
committed by GitHub
6 changed files with 143 additions and 59 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Improve CLI sidebar usage sections with cent-formatted costs, collapsible details, and aligned model totals.
@@ -52,8 +52,6 @@ const count = new Intl.NumberFormat("en-US")
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 6,
})
export function formatCount(value: number) {
@@ -68,6 +66,5 @@ export function formatRate(tokens: SessionModelUsage["totals"]["tokens"]) {
export function formatCost(input: number) {
const value = Math.max(0, Number.isFinite(input) ? input : 0)
if (value > 0 && value < 0.000001) return "<$0.000001"
return currency.format(value)
}
@@ -0,0 +1,39 @@
import type { RGBA } from "@opentui/core"
export function UsageRow(props: { label: string; value: string; color: RGBA }) {
return (
<box flexDirection="row" justifyContent="space-between">
<text fg={props.color}>{props.label}</text>
<text fg={props.color}>{props.value}</text>
</box>
)
}
export function ModelRow(props: {
label: string
steps: string
cost: string
expanded: boolean
text: RGBA
muted: RGBA
toggle: () => void
}) {
return (
<box flexDirection="row" gap={1} onMouseDown={props.toggle}>
<text fg={props.text} flexShrink={0}>
{props.expanded ? "▼" : "▶"}
</text>
<box flexGrow={1} minWidth={0} overflow="hidden">
<text fg={props.text} wrapMode="none">
<b>{props.label}</b>
</text>
</box>
<box width={5} flexDirection="row" flexShrink={0} justifyContent="flex-end">
<text fg={props.muted}>{props.steps}</text>
</box>
<box width={9} flexDirection="row" flexShrink={0} justifyContent="flex-end">
<text fg={props.muted}>{props.cost}</text>
</box>
</box>
)
}
@@ -15,11 +15,14 @@ import {
select,
type UsageResult,
} from "@/kilocode/plugins/model-usage"
import { ModelRow, UsageRow } from "@/kilocode/plugins/sidebar-usage-row"
const id = "internal:kilo-sidebar-usage"
function View(props: { api: TuiPluginApi; session_id: string }) {
const [usageOpen, setUsageOpen] = createSignal(true)
const [modelsOpen, setModelsOpen] = createSignal(true)
const [benchOpen, setBenchOpen] = createSignal(true)
const [expanded, setExpanded] = createSignal(new Set<string>())
const theme = () => props.api.theme.current
const local = useLocal()
@@ -42,10 +45,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
return provider?.models[current.modelID]?.terminalBench
})
const Row = (props: { label: string; value: string }) => (
<box flexDirection="row" justifyContent="space-between">
<text fg={theme().textMuted}>{props.label}</text>
<text fg={theme().textMuted}>{props.value}</text>
</box>
<UsageRow label={props.label} value={props.value} color={theme().textMuted} />
)
const toggle = (key: string) =>
setExpanded((current) => {
@@ -85,37 +85,31 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
return (
<box gap={1}>
<box>
<text fg={theme().text}>
<b>Token Usage</b>
</text>
<Show
when={usage()}
fallback={<text fg={theme().textMuted}>{unavailable() ? "Usage unavailable" : "Loading usage..."}</text>}
>
{(data) => (
<>
<Row label="Input" value={formatCount(data().totals.tokens.input)} />
<Row label="Output" value={formatCount(data().totals.tokens.output)} />
<Row label="Reasoning" value={formatCount(data().totals.tokens.reasoning)} />
<Row label="Cache read" value={formatCount(data().totals.tokens.cache.read)} />
<Row label="Cache write" value={formatCount(data().totals.tokens.cache.write)} />
<Row label="Cache rate" value={formatRate(data().totals.tokens)} />
<Row label="Cost" value={formatCost(data().totals.cost)} />
</>
)}
<box flexDirection="row" gap={1} onMouseDown={() => setUsageOpen((open) => !open)}>
<text fg={theme().text}>{usageOpen() ? "▼" : "▶"}</text>
<text fg={theme().text}>
<b>Token Usage</b>
</text>
</box>
<Show when={usageOpen()}>
<Show
when={usage()}
fallback={<text fg={theme().textMuted}>{unavailable() ? "Usage unavailable" : "Loading usage..."}</text>}
>
{(data) => (
<>
<Row label="Input" value={formatCount(data().totals.tokens.input)} />
<Row label="Output" value={formatCount(data().totals.tokens.output)} />
<Row label="Reasoning" value={formatCount(data().totals.tokens.reasoning)} />
<Row label="Cache read" value={formatCount(data().totals.tokens.cache.read)} />
<Row label="Cache write" value={formatCount(data().totals.tokens.cache.write)} />
<Row label="Cache rate" value={formatRate(data().totals.tokens)} />
<Row label="Cost" value={formatCost(data().totals.cost)} />
</>
)}
</Show>
</Show>
</box>
<Show when={bench()}>
{(value) => (
<box>
<text fg={theme().text}>
<b>Terminal Bench 2.0</b>
</text>
<Row label="Completion" value={fmtScore(value().overallScore)} />
<Row label="Cost / attempt" value={fmtAttemptCost(value().avgAttemptCostUsd)} />
</box>
)}
</Show>
<Show when={usage()}>
{(data) => (
<box>
@@ -140,10 +134,10 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
<text fg={theme().textMuted} flexGrow={1} minWidth={0} wrapMode="none">
Model
</text>
<box width={5} flexShrink={0} justifyContent="flex-end">
<box width={5} flexDirection="row" flexShrink={0} justifyContent="flex-end">
<text fg={theme().textMuted}>Steps</text>
</box>
<box width={9} flexShrink={0} justifyContent="flex-end">
<box width={9} flexDirection="row" flexShrink={0} justifyContent="flex-end">
<text fg={theme().textMuted}>Cost</text>
</box>
</box>
@@ -152,27 +146,18 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
const key = `${props.session_id}/${model.providerID}/${model.modelID}`
return (
<box>
<box flexDirection="row" gap={1} onMouseDown={() => toggle(key)}>
<text fg={theme().text} flexShrink={0}>
{expanded().has(key) ? "▼" : "▶"}
</text>
<box flexGrow={1} minWidth={0} overflow="hidden">
<text fg={theme().text} wrapMode="none">
<b>
{Locale.truncate(
RoutedModelMeta.label(providers(), model) ?? model.modelID,
19,
)}
</b>
</text>
</box>
<box width={5} flexShrink={0} justifyContent="flex-end">
<text fg={theme().textMuted}>{formatCount(model.steps)}</text>
</box>
<box width={9} flexShrink={0} justifyContent="flex-end">
<text fg={theme().textMuted}>{formatCost(model.cost)}</text>
</box>
</box>
<ModelRow
label={Locale.truncate(
RoutedModelMeta.label(providers(), model) ?? model.modelID,
19,
)}
steps={formatCount(model.steps)}
cost={formatCost(model.cost)}
expanded={expanded().has(key)}
text={theme().text}
muted={theme().textMuted}
toggle={() => toggle(key)}
/>
<Show when={expanded().has(key)}>
<box paddingLeft={2}>
<Row label="Input" value={formatCount(model.tokens.input)} />
@@ -197,6 +182,22 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
</box>
)}
</Show>
<Show when={bench()}>
{(value) => (
<box>
<box flexDirection="row" gap={1} onMouseDown={() => setBenchOpen((open) => !open)}>
<text fg={theme().text}>{benchOpen() ? "▼" : "▶"}</text>
<text fg={theme().text}>
<b>Terminal Bench 2.0</b>
</text>
</box>
<Show when={benchOpen()}>
<Row label="Completion" value={fmtScore(value().overallScore)} />
<Row label="Cost / attempt" value={fmtAttemptCost(value().avgAttemptCostUsd)} />
</Show>
</box>
)}
</Show>
</box>
)
}
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import type { Session } from "@kilocode/sdk/v2"
import {
failed,
formatCost,
formatRate,
groupModelsByProvider,
isSessionTreeMember,
@@ -85,4 +86,12 @@ describe("TUI model usage", () => {
])
expect(formatRate({ input: 100, output: 0, reasoning: 0, cache: { read: 300, write: 100 } })).toBe("60.0%")
})
test("formats costs to cents", () => {
expect(formatCost(18.382407)).toBe("$18.38")
expect(formatCost(12.166524)).toBe("$12.17")
expect(formatCost(0.0000001)).toBe("$0.00")
expect(formatCost(-1)).toBe("$0.00")
expect(formatCost(Number.NaN)).toBe("$0.00")
})
})
@@ -0,0 +1,33 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { testRender } from "@opentui/solid"
import { ModelRow, UsageRow } from "@/kilocode/plugins/sidebar-usage-row"
test("model costs align with usage values", async () => {
const app = await testRender(
() => (
<box width={36}>
<UsageRow label="Cost" value="$0.54" color={RGBA.fromHex("#ffffff")} />
<ModelRow
label="GPT-5.6 Sol"
steps="6"
cost="$0.40"
expanded={false}
text={RGBA.fromHex("#ffffff")}
muted={RGBA.fromHex("#ffffff")}
toggle={() => {}}
/>
</box>
),
{ width: 36, height: 3 },
)
try {
await app.renderOnce()
const lines = app.captureCharFrame().split("\n")
expect(lines[0]!.lastIndexOf("4")).toBe(lines[1]!.lastIndexOf("0"))
} finally {
app.renderer.destroy()
}
})