Skip to content
5 changes: 5 additions & 0 deletions .changeset/young-emus-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Implement better formatting for low cost values
13 changes: 7 additions & 6 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getModelMaxOutputTokens } from "@roo/api"
import { findLastIndex } from "@roo/array"

import { formatLargeNumber } from "@src/utils/format"
import { formatCost } from "@/utils/costFormatting"
import { cn } from "@src/lib/utils"
import { StandardTooltip, Button } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
Expand Down Expand Up @@ -261,19 +262,19 @@ const TaskHeader = ({
<div>
<div>
{t("chat:costs.totalWithSubtasks", {
cost: (aggregatedCost ?? totalCost).toFixed(2),
cost: formatCost(aggregatedCost ?? totalCost),
})}
</div>
{costBreakdown && <div className="text-xs mt-1">{costBreakdown}</div>}
</div>
) : (
<div>{t("chat:costs.total", { cost: totalCost.toFixed(2) })}</div>
<div>{t("chat:costs.total", { cost: formatCost(totalCost) })}</div>
)
}
side="top"
sideOffset={8}>
<span>
${(aggregatedCost ?? totalCost).toFixed(2)}
${formatCost(aggregatedCost ?? totalCost)}
{hasSubtasks && (
<span className="text-xs ml-1" title={t("chat:costs.includesSubtasks")}>
*
Expand Down Expand Up @@ -425,7 +426,7 @@ const TaskHeader = ({
<div>
<div>
{t("chat:costs.totalWithSubtasks", {
cost: (aggregatedCost ?? totalCost).toFixed(2),
cost: formatCost(aggregatedCost ?? totalCost),
})}
</div>
{costBreakdown && (
Expand All @@ -434,14 +435,14 @@ const TaskHeader = ({
</div>
) : (
<div>
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
{t("chat:costs.total", { cost: formatCost(totalCost) })}
</div>
)
}
side="top"
sideOffset={8}>
<span>
${(aggregatedCost ?? totalCost).toFixed(2)}
${formatCost(aggregatedCost ?? totalCost)}
{hasSubtasks && (
<span
className="text-xs ml-1"
Expand Down
10 changes: 9 additions & 1 deletion webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ describe("TaskHeader", () => {

it("should display cost when totalCost is greater than 0", () => {
renderTaskHeader()
expect(screen.getByText("$0.05")).toBeInTheDocument()
// formatCost(0.05) returns "0.0500" (4 decimal places since 0.05 is not > 0.05)
// The `$` and formatted number are separate text nodes, so match on the leaf element's
// combined textContent (avoids matching parent containers and causing "multiple elements").
const costEl = screen.getByText((_content, element) => {
if (!element) return false
if (element.children.length > 0) return false
return (element.textContent ?? "").replace(/\s+/g, "") === "$0.0500"
})
expect(costEl).toBeInTheDocument()
})

it("should not display cost when totalCost is 0", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { formatCost } from "@/utils/costFormatting"
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
import { FoldVertical } from "lucide-react"

Expand Down Expand Up @@ -41,7 +42,7 @@ export function CondensationResultRow({ data }: CondensationResultRowProps) {
{t("chat:contextManagement.tokens")}
</span>
<VSCodeBadge className={displayCost > 0 ? "opacity-100" : "opacity-0"}>
${displayCost.toFixed(2)}
${formatCost(displayCost)}
</VSCodeBadge>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/components/history/TaskItemFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import type { HistoryItem } from "@roo-code/types"
import { formatTimeAgo } from "@/utils/format"
import { formatCost } from "@/utils/costFormatting"
import { CopyButton } from "./CopyButton"
import { ExportButton } from "./ExportButton"
import { DeleteButton } from "./DeleteButton"
Expand All @@ -27,7 +28,7 @@ const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelect
{/* Cost */}
{!!item.totalCost && (
<span className="flex items-center" data-testid="cost-footer-compact">
{"$" + item.totalCost.toFixed(2)}
$${formatCost(item.totalCost)}
</span>
)}
</div>
Expand Down
42 changes: 40 additions & 2 deletions webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,25 @@ describe("TaskItem", () => {
vi.clearAllMocks()
})

it("renders task information", () => {
it("does not render cost for zero cost", () => {
const zeroCostTask = { ...mockTask, totalCost: 0.0 }
render(
<TaskItem
item={zeroCostTask}
variant="full"
isSelected={false}
onToggleSelection={vi.fn()}
isSelectionMode={false}
/>,
)

expect(screen.getByText("Test task")).toBeInTheDocument()
// The component does not render cost element when totalCost is 0 (falsy check)
expect(screen.queryByTestId("cost-footer-compact")).not.toBeInTheDocument()
})

it("renders low cost information", () => {
// Uses mockTask.totalCost (0.002)
render(
<TaskItem
item={mockTask}
Expand All @@ -43,7 +61,27 @@ describe("TaskItem", () => {
)

expect(screen.getByText("Test task")).toBeInTheDocument()
expect(screen.getByText("$0.00")).toBeInTheDocument() // Component shows $0.00 for small amounts
// The component shows 0.0020 for small amounts (4 decimal places via formatCost)
const costElement = screen.getByTestId("cost-footer-compact")
expect(costElement.textContent).toContain("0.0020")
})

it("renders high cost information", () => {
const highCostTask = { ...mockTask, totalCost: 0.0523 }
render(
<TaskItem
item={highCostTask}
variant="full"
isSelected={false}
onToggleSelection={vi.fn()}
isSelectionMode={false}
/>,
)

expect(screen.getByText("Test task")).toBeInTheDocument()
// The component shows 0.05 for high amounts (2 decimal places via formatCost)
const costElement = screen.getByTestId("cost-footer-compact")
expect(costElement.textContent).toContain("0.05")
})

it("handles selection in selection mode", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,35 @@ describe("TaskItemFooter", () => {
expect(screen.getByText(/ago/)).toBeInTheDocument()
})

it("renders cost information", () => {
it("does not render cost for zero cost", () => {
const zeroCostItem = {
...mockItem,
totalCost: 0.0,
}
render(<TaskItemFooter item={zeroCostItem} variant="full" />)

// The component does not render cost element when totalCost is 0 (falsy check)
expect(screen.queryByTestId("cost-footer-compact")).not.toBeInTheDocument()
})

it("renders low cost information", () => {
render(<TaskItemFooter item={mockItem} variant="full" />)

// The component shows $0.00 for small amounts, not the exact value
expect(screen.getByText("$0.00")).toBeInTheDocument()
// The component shows 0.0020 for small amounts (4 decimal places via formatCost)
const costElement = screen.getByTestId("cost-footer-compact")
expect(costElement.textContent).toContain("0.0020")
})

it("renders high cost information", () => {
const highCostItem = {
...mockItem,
totalCost: 0.0523,
}
render(<TaskItemFooter item={highCostItem} variant="full" />)

// The component shows 0.05 for high amounts (2 decimal places via formatCost)
const costElement = screen.getByTestId("cost-footer-compact")
expect(costElement.textContent).toContain("0.05")
})

it("shows action buttons", () => {
Expand Down
5 changes: 3 additions & 2 deletions webview-ui/src/components/kilocode/KiloTaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ClineMessage } from "@roo-code/types"
import { getModelMaxOutputTokens } from "@roo/api"

import { formatLargeNumber } from "@src/utils/format"
import { formatCost } from "@/utils/costFormatting"
import { cn } from "@src/lib/utils"
import { Button, StandardTooltip } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
Expand Down Expand Up @@ -150,7 +151,7 @@ const KiloTaskHeader = ({
{showDiffStats !== false && hasDiffStats && (
<DiffStatsDisplay added={diffStats.added} removed={diffStats.removed} />
)}
{!!totalCost && <span>${totalCost.toFixed(2)}</span>}
{!!totalCost && <span>${formatCost(totalCost)}</span>}
</div>
</div>
)}
Expand Down Expand Up @@ -248,7 +249,7 @@ const KiloTaskHeader = ({
<div className="flex justify-between items-center h-[20px]">
<div className="flex items-center gap-1">
<span className="font-bold">{t("chat:task.apiCost")}</span>
<span>${totalCost?.toFixed(2)}</span>
<span>${formatCost(totalCost)}</span>
</div>
<TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />
</div>
Expand Down
10 changes: 10 additions & 0 deletions webview-ui/src/utils/costFormatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ export function getCostBreakdownIfNeeded(
}
return formatCostBreakdown(costs.ownCost, costs.childrenCost, labels)
}

/**
* Formats a cost value: 2 decimal places if above $0.05, otherwise 4 decimal places.
* If cost value is zero it will stay at 2 decimal places.
* @param cost - The cost in USD
* @returns Formatted string like "0.0234" or "1.23"
*/
export function formatCost(cost: number): string {
return (cost === 0 || cost > 0.05) ? cost.toFixed(2) : cost.toFixed(4);
}
Loading