mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #12303 from Kilo-Org/fix/cli-sidebar-usage
fix(cli): improve sidebar usage display
This commit is contained in:
@@ -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()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user