Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 73 additions & 10 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
import { vscode } from "@src/utils/vscode"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { Button } from "@src/components/ui"
import { formatTokenStats } from "@src/utils/formatTokens"
import { Button, StandardTooltip } from "@src/components/ui"

import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
Expand Down Expand Up @@ -180,13 +181,20 @@ export const ChatRowContent = ({
vscode.postMessage({ type: "selectImages", context: "edit", messageTs: message.ts })
}, [message.ts])

const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, tokensIn, tokensOut, cacheReads] = useMemo(() => {
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
const info = safeJsonParse<ClineApiReqInfo>(message.text)
return [info?.cost, info?.cancelReason, info?.streamingFailedMessage]
return [
info?.cost,
info?.cancelReason,
info?.streamingFailedMessage,
info?.tokensIn,
info?.tokensOut,
info?.cacheReads,
]
}

return [undefined, undefined, undefined]
return [undefined, undefined, undefined, undefined, undefined, undefined]
}, [message.text, message.say])

// When resuming task, last wont be api_req_failed but a resume_task
Expand Down Expand Up @@ -1093,6 +1101,9 @@ export const ChatRowContent = ({
/>
)
case "api_req_started":
const tokenStats = formatTokenStats(tokensIn, tokensOut, cacheReads)
const hasTokenData = tokensIn !== undefined || tokensOut !== undefined

return (
<>
<div
Expand All @@ -1111,13 +1122,65 @@ export const ChatRowContent = ({
msUserSelect: "none",
}}
onClick={handleToggleExpand}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
minWidth: 0,
}}>
{icon}
{title}
<VSCodeBadge
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
<span
className="api-request-text"
style={{
display: "inline-block",
fontWeight: "bold",
color: "var(--vscode-foreground)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
flexShrink: 1,
minWidth: 0,
}}>
{title}
</span>
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }}>
{hasTokenData ? (
<StandardTooltip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered the accessibility of this tooltip for keyboard users? The StandardTooltip component might already handle keyboard accessibility, but it's worth verifying that users can access token statistics without a mouse.

content={
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span>↑ Input:</span>
<span className="font-mono">{tokenStats.input}</span>
</div>
<div className="flex items-center gap-2">
<span>↓ Output:</span>
<span className="font-mono">{tokenStats.output}</span>
</div>
</div>
}
side="top">
<VSCodeBadge
style={{
opacity:
cost !== null && cost !== undefined && cost > 0 ? 1 : 0,
flexShrink: 0,
cursor: "default",
}}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
</StandardTooltip>
) : (
<VSCodeBadge
style={{
opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0,
flexShrink: 0,
}}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
)}
</div>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
Expand Down
20 changes: 20 additions & 0 deletions webview-ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,24 @@ input[cmdk-input]:focus {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;

/* Hide API Request text when container is too narrow */
@media (max-width: 400px) {
.api-request-text {
display: none !important;
}
}

/* Alternative: Use container query for more precise control */
@supports (container-type: inline-size) {
.api-request-container {
container-type: inline-size;
}

@container (max-width: 350px) {
.api-request-text {
display: none !important;
}
}
}
}
52 changes: 52 additions & 0 deletions webview-ui/src/utils/formatTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Format token count for display
* @param count - The token count to format
* @returns Formatted string (e.g., "1.2k" for 1200)
*/
export function formatTokenCount(count: number | undefined): string {
if (count === undefined || count === 0) {
return "0"
}

if (count < 1000) {
return count.toString()
}

// Format as k (thousands) with one decimal place
const thousands = count / 1000
if (thousands < 10) {
// For values less than 10k, show one decimal place
return `${thousands.toFixed(1)}k`
} else {
// For values 10k and above, show no decimal places
return `${Math.round(thousands)}k`
}
}

/**
* Format token statistics for display
* @param tokensIn - Input tokens
* @param tokensOut - Output tokens
* @param cacheReads - Cache read tokens (optional)
* @returns Formatted string for display
*/
export function formatTokenStats(
tokensIn?: number,
tokensOut?: number,
cacheReads?: number,
): { input: string; output: string } {
let inputDisplay = formatTokenCount(tokensIn)

// Add cache reads in parentheses if they exist
if (cacheReads && cacheReads > 0) {
const cacheDisplay = formatTokenCount(cacheReads)
inputDisplay = `${inputDisplay} (${cacheDisplay} cache)`
}

const outputDisplay = formatTokenCount(tokensOut)

return {
input: inputDisplay,
output: outputDisplay,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding unit tests for these formatting functions. They handle several edge cases (undefined, 0, < 1000, < 10k, >= 10k) that would benefit from test coverage to ensure the formatting logic works correctly.

You could create a test file at with tests for both functions.

Loading