From 643da52470a6cd1592ca2ed85ed30aade6f3d451 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 6 Sep 2025 16:00:06 +0000 Subject: [PATCH 01/11] feat: Add token statistics display to API requests (fixes #7740) --- webview-ui/src/components/chat/ChatRow.tsx | 37 +++++++++++++-- webview-ui/src/utils/formatTokens.ts | 52 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/utils/formatTokens.ts diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 7b3107a2bed0..12a45369fa0b 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -17,6 +17,7 @@ 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 { formatTokenStats } from "@src/utils/formatTokens" import { Button } from "@src/components/ui" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" @@ -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(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 @@ -1093,6 +1101,9 @@ export const ChatRowContent = ({ /> ) case "api_req_started": + const tokenStats = formatTokenStats(tokensIn, tokensOut, cacheReads) + const hasTokenData = tokensIn !== undefined || tokensOut !== undefined + return ( <>
0 ? 1 : 0 }}> ${Number(cost || 0)?.toFixed(4)} + {hasTokenData && ( +
+ + ↑ {tokenStats.input} + + + ↓ {tokenStats.output} + +
+ )}
diff --git a/webview-ui/src/utils/formatTokens.ts b/webview-ui/src/utils/formatTokens.ts new file mode 100644 index 000000000000..2e11f16af8d1 --- /dev/null +++ b/webview-ui/src/utils/formatTokens.ts @@ -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, + } +} From 333e1e455a01602b6bb5ef6c4fa1873bb7eabc4e Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 6 Sep 2025 17:15:16 +0000 Subject: [PATCH 02/11] fix: prevent token stats overflow in narrow windows - Removed flex-wrap from token stats container to keep items on single line - Added flexShrink: 0 to prevent badge and stats from shrinking - Added whiteSpace: nowrap to maintain single-line display - Grouped cost badge and token stats in separate container for better layout control - Added minWidth: 0 to parent container for proper text truncation This fixes the UI overflow issue where the price would break into two lines and overflow the gray box in narrow window widths. --- webview-ui/src/components/chat/ChatRow.tsx | 62 ++++++++++++++-------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 12a45369fa0b..423660c699f4 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1122,33 +1122,49 @@ export const ChatRowContent = ({ msUserSelect: "none", }} onClick={handleToggleExpand}> -
+
{icon} {title} - 0 ? 1 : 0 }}> - ${Number(cost || 0)?.toFixed(4)} - - {hasTokenData && ( -
- - ↑ {tokenStats.input} - - + 0 ? 1 : 0, + flexShrink: 0, + }}> + ${Number(cost || 0)?.toFixed(4)} + + {hasTokenData && ( +
- ↓ {tokenStats.output} - -
- )} + + ↑ {tokenStats.input} + + + ↓ {tokenStats.output} + +
+ )} +
From e89cd7bb037328dc5f64bbcf600b265e8a6f8914 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 6 Sep 2025 17:25:42 +0000 Subject: [PATCH 03/11] fix: Hide "API Request" text when container width is limited - Wrapped API Request text in a span with class api-request-text - Added responsive CSS to hide text on narrow screens (< 400px) - Added container query support for more precise control - Maintains visibility of cost badge and token statistics - Prevents text truncation issues in narrow windows --- webview-ui/src/components/chat/ChatRow.tsx | 77 +++++++++++++++------- webview-ui/src/index.css | 20 ++++++ 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 12a45369fa0b..e0415306d1fe 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1122,33 +1122,62 @@ export const ChatRowContent = ({ msUserSelect: "none", }} onClick={handleToggleExpand}> -
+
{icon} - {title} - 0 ? 1 : 0 }}> - ${Number(cost || 0)?.toFixed(4)} - - {hasTokenData && ( -
- - ↑ {tokenStats.input} - - + {title} + +
+ 0 ? 1 : 0, + flexShrink: 0, + }}> + ${Number(cost || 0)?.toFixed(4)} + + {hasTokenData && ( +
- ↓ {tokenStats.output} - -
- )} + + ↑ {tokenStats.input} + + + ↓ {tokenStats.output} + +
+ )} +
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index ba7aeb576eea..073b815845aa 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -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; + } + } + } } From 1666dd51f22c3099b51fa0581938983df0223e9e Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 8 Sep 2025 23:12:28 +0000 Subject: [PATCH 04/11] feat: Move token statistics to tooltip on price hover - Wrapped cost badge with StandardTooltip component - Token statistics (input/output/cache) now shown in tooltip - Improved UX by reducing visual clutter while keeping info accessible - Added cursor:default to badge when hovering As suggested by @daniel-lxs in PR #7741 --- webview-ui/src/components/chat/ChatRow.tsx | 59 ++++++++++++---------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index e0415306d1fe..43ccb7d9d50c 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -18,7 +18,7 @@ import { vscode } from "@src/utils/vscode" import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric" import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { formatTokenStats } from "@src/utils/formatTokens" -import { Button } from "@src/components/ui" +import { Button, StandardTooltip } from "@src/components/ui" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" @@ -1146,36 +1146,39 @@ export const ChatRowContent = ({ {title}
- 0 ? 1 : 0, - flexShrink: 0, - }}> - ${Number(cost || 0)?.toFixed(4)} - - {hasTokenData && ( -
+
+ ↑ Input: + {tokenStats.input} +
+
+ ↓ Output: + {tokenStats.output} +
+
+ } + side="top"> + 0 ? 1 : 0, + flexShrink: 0, + cursor: "default", + }}> + ${Number(cost || 0)?.toFixed(4)} + + + ) : ( + 0 ? 1 : 0, flexShrink: 0, - whiteSpace: "nowrap", }}> - - ↑ {tokenStats.input} - - - ↓ {tokenStats.output} - -
+ ${Number(cost || 0)?.toFixed(4)} + )} From 4a483886c3d827dd381e5e28e57b8f7d1cd202f8 Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov Date: Fri, 12 Sep 2025 10:23:37 +0300 Subject: [PATCH 05/11] feat: Complete i18n localization for token statistics Fix i18n violations by adding complete translations for token statistics tooltip. Changes include: - Add tokenStats section with inputLabel, outputLabel, and cacheLabel to all 18 language files - Replace hardcoded strings in ChatRow.tsx with i18n calls - Update formatTokens.ts to accept localized cache label parameter - Translate shareSuccessPublic message to all supported languages - Ensure proper localization for token usage display in API request tooltips This resolves the automated code review rule violation (irule_C0ez7Rji6ANcGkkX) and provides complete localization support for the token statistics feature. --- webview-ui/src/components/chat/ChatRow.tsx | 11 ++++++++--- webview-ui/src/i18n/locales/ca/chat.json | 7 ++++++- webview-ui/src/i18n/locales/de/chat.json | 7 ++++++- webview-ui/src/i18n/locales/en/chat.json | 7 ++++++- webview-ui/src/i18n/locales/es/chat.json | 7 ++++++- webview-ui/src/i18n/locales/fr/chat.json | 7 ++++++- webview-ui/src/i18n/locales/hi/chat.json | 7 ++++++- webview-ui/src/i18n/locales/id/chat.json | 7 ++++++- webview-ui/src/i18n/locales/it/chat.json | 7 ++++++- webview-ui/src/i18n/locales/ja/chat.json | 7 ++++++- webview-ui/src/i18n/locales/ko/chat.json | 7 ++++++- webview-ui/src/i18n/locales/nl/chat.json | 7 ++++++- webview-ui/src/i18n/locales/pl/chat.json | 7 ++++++- webview-ui/src/i18n/locales/pt-BR/chat.json | 7 ++++++- webview-ui/src/i18n/locales/ru/chat.json | 7 ++++++- webview-ui/src/i18n/locales/tr/chat.json | 7 ++++++- webview-ui/src/i18n/locales/vi/chat.json | 7 ++++++- webview-ui/src/i18n/locales/zh-CN/chat.json | 7 ++++++- webview-ui/src/i18n/locales/zh-TW/chat.json | 7 ++++++- webview-ui/src/utils/formatTokens.ts | 4 +++- 20 files changed, 119 insertions(+), 22 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 43ccb7d9d50c..f247ca5effe4 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1101,7 +1101,12 @@ export const ChatRowContent = ({ /> ) case "api_req_started": - const tokenStats = formatTokenStats(tokensIn, tokensOut, cacheReads) + const tokenStats = formatTokenStats( + tokensIn, + tokensOut, + cacheReads, + t("chat:task.tokenStats.cacheLabel"), + ) const hasTokenData = tokensIn !== undefined || tokensOut !== undefined return ( @@ -1151,11 +1156,11 @@ export const ChatRowContent = ({ content={
- ↑ Input: + {t("chat:task.tokenStats.inputLabel")} {tokenStats.input}
- ↓ Output: + {t("chat:task.tokenStats.outputLabel")} {tokenStats.output}
diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 1d20be0c4077..f39bff64d45b 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Inicia sessió a Roo Code Cloud per compartir tasques", "sharingDisabledByOrganization": "Compartició deshabilitada per l'organització", "shareSuccessOrganization": "Enllaç d'organització copiat al porta-retalls", - "shareSuccessPublic": "Enllaç públic copiat al porta-retalls" + "shareSuccessPublic": "Enllaç públic copiat al porta-retalls", + "tokenStats": { + "inputLabel": "↑ Entrada:", + "outputLabel": "↓ Sortida:", + "cacheLabel": "caché" + } }, "unpin": "Desfixar", "pin": "Fixar", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 82f1c77fbf1f..b168321dc944 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Melde dich bei Roo Code Cloud an, um Aufgaben zu teilen", "sharingDisabledByOrganization": "Freigabe von der Organisation deaktiviert", "shareSuccessOrganization": "Organisationslink in die Zwischenablage kopiert", - "shareSuccessPublic": "Öffentlicher Link in die Zwischenablage kopiert" + "shareSuccessPublic": "Öffentlicher Link in die Zwischenablage kopiert", + "tokenStats": { + "inputLabel": "↑ Eingabe:", + "outputLabel": "↓ Ausgabe:", + "cacheLabel": "Cache" + } }, "unpin": "Lösen von oben", "pin": "Anheften", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 72eacc5c5829..3271a5db27d3 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Sign in to Roo Code Cloud to share tasks", "sharingDisabledByOrganization": "Sharing disabled by organization", "shareSuccessOrganization": "Organization link copied to clipboard", - "shareSuccessPublic": "Public link copied to clipboard" + "shareSuccessPublic": "Public link copied to clipboard", + "tokenStats": { + "inputLabel": "↑ Input:", + "outputLabel": "↓ Output:", + "cacheLabel": "cache" + } }, "unpin": "Unpin", "pin": "Pin", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index e63731b0954d..62ff1b89b109 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Inicia sesión en Roo Code Cloud para compartir tareas", "sharingDisabledByOrganization": "Compartir deshabilitado por la organización", "shareSuccessOrganization": "Enlace de organización copiado al portapapeles", - "shareSuccessPublic": "Enlace público copiado al portapapeles" + "shareSuccessPublic": "Enlace público copiado al portapapeles", + "tokenStats": { + "inputLabel": "↑ Entrada:", + "outputLabel": "↓ Salida:", + "cacheLabel": "caché" + } }, "unpin": "Desfijar", "pin": "Fijar", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 257548978729..1451858f51b2 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Connecte-toi à Roo Code Cloud pour partager des tâches", "sharingDisabledByOrganization": "Partage désactivé par l'organisation", "shareSuccessOrganization": "Lien d'organisation copié dans le presse-papiers", - "shareSuccessPublic": "Lien public copié dans le presse-papiers" + "shareSuccessPublic": "Lien public copié dans le presse-papiers", + "tokenStats": { + "inputLabel": "↑ Entrée :", + "outputLabel": "↓ Sortie :", + "cacheLabel": "cache" + } }, "unpin": "Désépingler", "pin": "Épingler", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 28fc26fcaf46..3407c91e4da6 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "कार्य साझा करने के लिए Roo Code Cloud में साइन इन करें", "sharingDisabledByOrganization": "संगठन द्वारा साझाकरण अक्षम किया गया", "shareSuccessOrganization": "संगठन लिंक क्लिपबोर्ड में कॉपी किया गया", - "shareSuccessPublic": "सार्वजनिक लिंक क्लिपबोर्ड में कॉपी किया गया" + "shareSuccessPublic": "सार्वजनिक लिंक क्लिपबोर्ड में कॉपी किया गया", + "tokenStats": { + "inputLabel": "↑ इनपुट:", + "outputLabel": "↓ आउटपुट:", + "cacheLabel": "कैश" + } }, "unpin": "पिन करें", "pin": "अवपिन करें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 0425a02b8f26..a7ff64371193 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Masuk ke Roo Code Cloud untuk berbagi tugas", "sharingDisabledByOrganization": "Berbagi dinonaktifkan oleh organisasi", "shareSuccessOrganization": "Tautan organisasi disalin ke clipboard", - "shareSuccessPublic": "Tautan publik disalin ke clipboard" + "shareSuccessPublic": "Tautan publik disalin ke clipboard", + "tokenStats": { + "inputLabel": "↑ Masukan:", + "outputLabel": "↓ Keluaran:", + "cacheLabel": "cache" + } }, "history": { "title": "Riwayat" diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 4dd1270e3498..9d04de772087 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Accedi a Roo Code Cloud per condividere attività", "sharingDisabledByOrganization": "Condivisione disabilitata dall'organizzazione", "shareSuccessOrganization": "Link organizzazione copiato negli appunti", - "shareSuccessPublic": "Link pubblico copiato negli appunti" + "shareSuccessPublic": "Link pubblico copiato negli appunti", + "tokenStats": { + "inputLabel": "↑ Input:", + "outputLabel": "↓ Output:", + "cacheLabel": "cache" + } }, "unpin": "Rilascia", "pin": "Fissa", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 9a5d47fec890..d16d846ce444 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "タスクを共有するためにRoo Code Cloudにサインイン", "sharingDisabledByOrganization": "組織により共有が無効化されています", "shareSuccessOrganization": "組織リンクをクリップボードにコピーしました", - "shareSuccessPublic": "公開リンクをクリップボードにコピーしました" + "shareSuccessPublic": "パブリックリンクをクリップボードにコピーしました", + "tokenStats": { + "inputLabel": "↑ 入力:", + "outputLabel": "↓ 出力:", + "cacheLabel": "キャッシュ" + } }, "unpin": "ピン留めを解除", "pin": "ピン留め", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index aaf29243b702..261d59d65b08 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "작업을 공유하려면 Roo Code Cloud에 로그인하세요", "sharingDisabledByOrganization": "조직에서 공유가 비활성화됨", "shareSuccessOrganization": "조직 링크가 클립보드에 복사되었습니다", - "shareSuccessPublic": "공개 링크가 클립보드에 복사되었습니다" + "shareSuccessPublic": "공개 링크가 클립보드에 복사되었습니다", + "tokenStats": { + "inputLabel": "↑ 입력:", + "outputLabel": "↓ 출력:", + "cacheLabel": "캐시" + } }, "unpin": "고정 해제하기", "pin": "고정하기", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index c6d52fa92ed2..f164cbcf4444 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Meld je aan bij Roo Code Cloud om taken te delen", "sharingDisabledByOrganization": "Delen uitgeschakeld door organisatie", "shareSuccessOrganization": "Organisatielink gekopieerd naar klembord", - "shareSuccessPublic": "Openbare link gekopieerd naar klembord" + "shareSuccessPublic": "Openbare link gekopieerd naar klembord", + "tokenStats": { + "inputLabel": "↑ Input:", + "outputLabel": "↓ Output:", + "cacheLabel": "cache" + } }, "unpin": "Losmaken", "pin": "Vastmaken", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 2028cb705b34..ffa5f99e3da0 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Zaloguj się do Roo Code Cloud, aby udostępniać zadania", "sharingDisabledByOrganization": "Udostępnianie wyłączone przez organizację", "shareSuccessOrganization": "Link organizacji skopiowany do schowka", - "shareSuccessPublic": "Link publiczny skopiowany do schowka" + "shareSuccessPublic": "Link publiczny skopiowany do schowka", + "tokenStats": { + "inputLabel": "↑ Wejście:", + "outputLabel": "↓ Wyjście:", + "cacheLabel": "pamięć podręczna" + } }, "unpin": "Odepnij", "pin": "Przypnij", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 6ee23ca62746..35ff7acf3279 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Entre no Roo Code Cloud para compartilhar tarefas", "sharingDisabledByOrganization": "Compartilhamento desabilitado pela organização", "shareSuccessOrganization": "Link da organização copiado para a área de transferência", - "shareSuccessPublic": "Link público copiado para a área de transferência" + "shareSuccessPublic": "Link público copiado para a área de transferência", + "tokenStats": { + "inputLabel": "↑ Entrada:", + "outputLabel": "↓ Saída:", + "cacheLabel": "cache" + } }, "unpin": "Desfixar", "pin": "Fixar", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 6cafe6bac991..158884c2c8a4 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Войди в Roo Code Cloud, чтобы делиться задачами", "sharingDisabledByOrganization": "Обмен отключен организацией", "shareSuccessOrganization": "Ссылка организации скопирована в буфер обмена", - "shareSuccessPublic": "Публичная ссылка скопирована в буфер обмена" + "shareSuccessPublic": "Публичная ссылка скопирована в буфер обмена", + "tokenStats": { + "inputLabel": "↑ Вход:", + "outputLabel": "↓ Выход:", + "cacheLabel": "кэш" + } }, "unpin": "Открепить", "pin": "Закрепить", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 867acfbc9fb8..63320160f1ea 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Görevleri paylaşmak için Roo Code Cloud'a giriş yap", "sharingDisabledByOrganization": "Paylaşım kuruluş tarafından devre dışı bırakıldı", "shareSuccessOrganization": "Organizasyon bağlantısı panoya kopyalandı", - "shareSuccessPublic": "Genel bağlantı panoya kopyalandı" + "shareSuccessPublic": "Herkese açık bağlantı panoya kopyalandı", + "tokenStats": { + "inputLabel": "↑ Giriş:", + "outputLabel": "↓ Çıkış:", + "cacheLabel": "önbellek" + } }, "unpin": "Sabitlemeyi iptal et", "pin": "Sabitle", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index ef8e951aace2..b3c11a98a3fa 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "Đăng nhập vào Roo Code Cloud để chia sẻ tác vụ", "sharingDisabledByOrganization": "Chia sẻ bị tổ chức vô hiệu hóa", "shareSuccessOrganization": "Liên kết tổ chức đã được sao chép vào clipboard", - "shareSuccessPublic": "Liên kết công khai đã được sao chép vào clipboard" + "shareSuccessPublic": "Liên kết công khai đã được sao chép vào clipboard", + "tokenStats": { + "inputLabel": "↑ Đầu vào:", + "outputLabel": "↓ Đầu ra:", + "cacheLabel": "bộ nhớ cache" + } }, "unpin": "Bỏ ghim khỏi đầu", "pin": "Ghim lên đầu", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 1e430200a1be..c2318e02f298 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "登录 Roo Code Cloud 以分享任务", "sharingDisabledByOrganization": "组织已禁用分享功能", "shareSuccessOrganization": "组织链接已复制到剪贴板", - "shareSuccessPublic": "公开链接已复制到剪贴板" + "shareSuccessPublic": "公共链接已复制到剪贴板", + "tokenStats": { + "inputLabel": "↑ 输入:", + "outputLabel": "↓ 输出:", + "cacheLabel": "缓存" + } }, "unpin": "取消置顶", "pin": "置顶", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index f5183d65a948..bbe2f8008451 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -24,7 +24,12 @@ "connectToCloudDescription": "登入 Roo Code Cloud 以分享工作", "sharingDisabledByOrganization": "組織已停用分享功能", "shareSuccessOrganization": "組織連結已複製到剪貼簿", - "shareSuccessPublic": "公開連結已複製到剪貼簿" + "shareSuccessPublic": "公開連結已複製到剪貼簿", + "tokenStats": { + "inputLabel": "↑ 輸入:", + "outputLabel": "↓ 輸出:", + "cacheLabel": "快取" + } }, "unpin": "取消釘選", "pin": "釘選", diff --git a/webview-ui/src/utils/formatTokens.ts b/webview-ui/src/utils/formatTokens.ts index 2e11f16af8d1..2225e4e1f8b5 100644 --- a/webview-ui/src/utils/formatTokens.ts +++ b/webview-ui/src/utils/formatTokens.ts @@ -28,19 +28,21 @@ export function formatTokenCount(count: number | undefined): string { * @param tokensIn - Input tokens * @param tokensOut - Output tokens * @param cacheReads - Cache read tokens (optional) + * @param cacheLabel - Localized cache label * @returns Formatted string for display */ export function formatTokenStats( tokensIn?: number, tokensOut?: number, cacheReads?: number, + cacheLabel: string = "cache", ): { 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)` + inputDisplay = `${inputDisplay} (${cacheDisplay} ${cacheLabel})` } const outputDisplay = formatTokenCount(tokensOut) From a2abad10de3afc0a2da69cbc8283731ac3d99483 Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov Date: Fri, 12 Sep 2025 10:29:24 +0300 Subject: [PATCH 06/11] Fix JSDoc return type for formatTokenStats function The @returns comment incorrectly described the function as returning a formatted string for display, but it actually returns an object with 'input' and 'output' strings. Corrected for accuracy. --- webview-ui/src/utils/formatTokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/utils/formatTokens.ts b/webview-ui/src/utils/formatTokens.ts index 2225e4e1f8b5..dd79afd47a3f 100644 --- a/webview-ui/src/utils/formatTokens.ts +++ b/webview-ui/src/utils/formatTokens.ts @@ -29,7 +29,7 @@ export function formatTokenCount(count: number | undefined): string { * @param tokensOut - Output tokens * @param cacheReads - Cache read tokens (optional) * @param cacheLabel - Localized cache label - * @returns Formatted string for display + * @returns { { input: string; output: string } } An object with formatted input and output token strings. */ export function formatTokenStats( tokensIn?: number, From da544a023ee89eff7f31541ce8533ae71bbb652e Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov Date: Fri, 12 Sep 2025 10:51:51 +0300 Subject: [PATCH 07/11] Fix rules incorrectly nested in the media container The media query and container queries were incorrectly nested inside the .transition-colors utility class in webview-ui/src/index.css, which would limit their scope unintentionally. Fixed it by moving them to the root level of the CSS file, right after the .transition-colors rule. This should ensure the responsive hiding of the API request text applies globally as intended. --- webview-ui/src/index.css | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 073b815845aa..a25cd94ed42e 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -486,24 +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; - } +/* 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; - } +/* 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; - } + @container (max-width: 350px) { + .api-request-text { + display: none !important; } } } From 8fa4389ff678b2a5d34b59bc618b6c8d5cfe40e4 Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov Date: Fri, 12 Sep 2025 11:39:28 +0300 Subject: [PATCH 08/11] Fix: Add missing Catalan translations for token stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing translation keys for token statistics in Catalan locale Changes: - Add task.tokenStats.inputLabel: "↑ Entrada:" - Add task.tokenStats.outputLabel: "↓ Sortida:" - Add task.tokenStats.cacheLabel: "caché" This fixes the CI check-translations failure for Catalan language. --- webview-ui/src/i18n/locales/ca/chat.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index d0d2bbc1f542..fa9a765f9ed5 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -26,7 +26,12 @@ "shareSuccessOrganization": "Enllaç d'organització copiat al porta-retalls", "shareSuccessPublic": "Enllaç públic copiat al porta-retalls", "openInCloud": "Obrir tasca a Roo Code Cloud", - "openInCloudIntro": "Continua monitoritzant o interactuant amb Roo des de qualsevol lloc. Escaneja, fes clic o copia per obrir." + "openInCloudIntro": "Continua monitoritzant o interactuant amb Roo des de qualsevol lloc. Escaneja, fes clic o copia per obrir.", + "tokenStats": { + "inputLabel": "↑ Entrada:", + "outputLabel": "↓ Sortida:", + "cacheLabel": "caché" + } }, "unpin": "Desfixar", "pin": "Fixar", From b9b8dd524c4bf40e15bf0694fec5fb969333c649 Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov Date: Fri, 12 Sep 2025 12:03:40 +0300 Subject: [PATCH 09/11] feat: Add tooltip to API request text when cost is hidden Refactor tooltip behavior in ChatRow component to show token stats on hover over api-request-text span when the cost badge is not displayed. Changes include: - Add conditional tooltip rendering on span when cost <= 0 - Extract shared constants to eliminate code duplication - Keep tooltip on badge when cost > 0 - Maintain original cursor behavior --- webview-ui/src/components/chat/ChatRow.tsx | 100 +++++++++++---------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index b9df9352efd8..7c4895552a73 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1109,6 +1109,46 @@ export const ChatRowContent = ({ ) const hasTokenData = tokensIn !== undefined || tokensOut !== undefined + const showPrice = cost !== null && cost !== undefined && cost > 0 + + const tooltipContent = ( +
+
+ {t("chat:task.tokenStats.inputLabel")} + {tokenStats.input} +
+
+ {t("chat:task.tokenStats.outputLabel")} + {tokenStats.output} +
+
+ ) + + const titleSpan = ( + + {title} + + ) + + const badgeStyle = { + opacity: showPrice ? 1 : 0, + flexShrink: 0, + ...(hasTokenData ? { cursor: "default" } : {}), + } + + const costBadge = ${Number(cost || 0)?.toFixed(4)} + return ( <>
{icon} - - {title} - + {hasTokenData && !showPrice ? ( + + {titleSpan} + + ) : ( + titleSpan + )}
{hasTokenData ? ( - -
- {t("chat:task.tokenStats.inputLabel")} - {tokenStats.input} -
-
- {t("chat:task.tokenStats.outputLabel")} - {tokenStats.output} -
-
- } - side="top"> - 0 ? 1 : 0, - flexShrink: 0, - cursor: "default", - }}> - ${Number(cost || 0)?.toFixed(4)} - - + showPrice ? ( + + {costBadge} + + ) : ( + costBadge + ) ) : ( - 0 ? 1 : 0, - flexShrink: 0, - }}> - ${Number(cost || 0)?.toFixed(4)} - + costBadge )}
From c6fd34efbc63f8c76b11a5e268ccaaeca26b63db Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov <14850941+mikhailsal@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:44:34 +0300 Subject: [PATCH 10/11] Refactor: Eliminate duplicate token formatting code Replace custom token formatting logic with existing formatLargeNumber function. Changes: - Use formatLargeNumber from utils/format.ts instead of custom implementation - Maintain same interface and undefined handling - Reduce code duplication across the project - Add import for formatLargeNumber utility --- webview-ui/src/utils/formatTokens.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/webview-ui/src/utils/formatTokens.ts b/webview-ui/src/utils/formatTokens.ts index dd79afd47a3f..50ab08933b7b 100644 --- a/webview-ui/src/utils/formatTokens.ts +++ b/webview-ui/src/utils/formatTokens.ts @@ -1,3 +1,5 @@ +import { formatLargeNumber } from "./format" + /** * Format token count for display * @param count - The token count to format @@ -7,20 +9,7 @@ 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` - } + return formatLargeNumber(count) } /** @@ -39,7 +28,6 @@ export function formatTokenStats( ): { 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} ${cacheLabel})` From d6ce827ea3506cd9edc9c5820693e42fad55eaa9 Mon Sep 17 00:00:00 2001 From: Mikhail Salnikov <14850941+mikhailsal@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:03:52 +0300 Subject: [PATCH 11/11] Add comprehensive unit tests for formatTokens.ts Created formatTokens.spec.ts with 21 tests covering: - formatTokenCount function with all edge cases - formatTokenStats function with cache and parameter combinations - 100% code coverage (statements, branches, functions, lines) Tests verify proper token formatting, cache handling, and error cases. --- .../src/utils/__tests__/formatTokens.spec.ts | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 webview-ui/src/utils/__tests__/formatTokens.spec.ts diff --git a/webview-ui/src/utils/__tests__/formatTokens.spec.ts b/webview-ui/src/utils/__tests__/formatTokens.spec.ts new file mode 100644 index 000000000000..28c2cc01a3be --- /dev/null +++ b/webview-ui/src/utils/__tests__/formatTokens.spec.ts @@ -0,0 +1,197 @@ +import { describe, it, expect, vi } from "vitest" +import { formatTokenCount, formatTokenStats } from "../formatTokens" + +// Mock i18next (same as in format.spec.ts) +vi.mock("i18next", () => ({ + default: { + t: vi.fn((key: string, options?: any) => { + // Mock translations for testing + const translations: Record = { + "common:number_format.billion_suffix": "b", + "common:number_format.million_suffix": "m", + "common:number_format.thousand_suffix": "k", + } + + let result = translations[key] || key + if (options?.count !== undefined) { + result = result.replace("{{count}}", options.count.toString()) + } + return result + }), + language: "en", + }, +})) + +// Mock formatLargeNumber +vi.mock("../format", () => ({ + formatLargeNumber: vi.fn((num: number) => { + if (num >= 1e9) { + return (num / 1e9).toFixed(1) + "b" + } + if (num >= 1e6) { + return (num / 1e6).toFixed(1) + "m" + } + if (num >= 1e3) { + return (num / 1e3).toFixed(1) + "k" + } + return num.toString() + }), +})) + +describe("formatTokenCount", () => { + it("should return '0' for undefined count", () => { + expect(formatTokenCount(undefined)).toBe("0") + }) + + it("should return '0' for count of 0", () => { + expect(formatTokenCount(0)).toBe("0") + }) + + it("should format small numbers as strings", () => { + expect(formatTokenCount(42)).toBe("42") + expect(formatTokenCount(999)).toBe("999") + }) + + it("should format thousands correctly", () => { + expect(formatTokenCount(1500)).toBe("1.5k") + expect(formatTokenCount(2000)).toBe("2.0k") + }) + + it("should format millions correctly", () => { + expect(formatTokenCount(1500000)).toBe("1.5m") + expect(formatTokenCount(2000000)).toBe("2.0m") + }) + + it("should format billions correctly", () => { + expect(formatTokenCount(1500000000)).toBe("1.5b") + expect(formatTokenCount(2000000000)).toBe("2.0b") + }) +}) + +describe("formatTokenStats", () => { + describe("without cache reads", () => { + it("should format input and output tokens without cache", () => { + const result = formatTokenStats(1000, 500) + expect(result).toEqual({ + input: "1.0k", + output: "500", + }) + }) + + it("should handle undefined tokens", () => { + const result = formatTokenStats(undefined, undefined) + expect(result).toEqual({ + input: "0", + output: "0", + }) + }) + + it("should handle zero tokens", () => { + const result = formatTokenStats(0, 0) + expect(result).toEqual({ + input: "0", + output: "0", + }) + }) + + it("should handle only input tokens", () => { + const result = formatTokenStats(2000, undefined) + expect(result).toEqual({ + input: "2.0k", + output: "0", + }) + }) + + it("should handle only output tokens", () => { + const result = formatTokenStats(undefined, 3000) + expect(result).toEqual({ + input: "0", + output: "3.0k", + }) + }) + + it("should handle large numbers", () => { + const result = formatTokenStats(1500000, 2000000) + expect(result).toEqual({ + input: "1.5m", + output: "2.0m", + }) + }) + }) + + describe("with cache reads", () => { + it("should include cache reads in input display with default label", () => { + const result = formatTokenStats(1000, 500, 200) + expect(result).toEqual({ + input: "1.0k (200 cache)", + output: "500", + }) + }) + + it("should include cache reads in input display with custom label", () => { + const result = formatTokenStats(1000, 500, 200, "cached") + expect(result).toEqual({ + input: "1.0k (200 cached)", + output: "500", + }) + }) + + it("should handle cache reads with large numbers", () => { + const result = formatTokenStats(1000000, 500000, 200000, "cache") + expect(result).toEqual({ + input: "1.0m (200.0k cache)", + output: "500.0k", + }) + }) + + it("should handle zero cache reads (should not display cache)", () => { + const result = formatTokenStats(1000, 500, 0) + expect(result).toEqual({ + input: "1.0k", + output: "500", + }) + }) + + it("should handle undefined cache reads (should not display cache)", () => { + const result = formatTokenStats(1000, 500, undefined) + expect(result).toEqual({ + input: "1.0k", + output: "500", + }) + }) + + it("should handle negative cache reads (should not display cache)", () => { + const result = formatTokenStats(1000, 500, -100) + expect(result).toEqual({ + input: "1.0k", + output: "500", + }) + }) + }) + + describe("edge cases", () => { + it("should handle very large numbers", () => { + const result = formatTokenStats(5000000000, 1000000000) + expect(result).toEqual({ + input: "5.0b", + output: "1.0b", + }) + }) + + it("should handle decimal numbers", () => { + const result = formatTokenStats(1234.56, 567.89) + expect(result).toEqual({ + input: "1.2k", + output: "567.89", // formatLargeNumber preserves decimals for small numbers + }) + }) + + it("should handle empty cache label", () => { + const result = formatTokenStats(1000, 500, 200, "") + expect(result).toEqual({ + input: "1.0k (200 )", + output: "500", + }) + }) + }) +})