Просмотр исходного кода

feat: Experiment: Show a bit of stats in Cloud tab to help users discover there's more in Cloud (#8415)

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <[email protected]>
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: SannidhyaSah <[email protected]>
Co-authored-by: John Richmond <[email protected]>
Bruno Bergher 2 месяцев назад
Родитель
Сommit
cd8036d2d8
48 измененных файлов с 691 добавлено и 142 удалено
  1. 29 7
      packages/cloud/src/CloudAPI.ts
  2. 63 0
      src/core/webview/webviewMessageHandler.ts
  3. 2 0
      src/shared/ExtensionMessage.ts
  4. 1 0
      src/shared/WebviewMessage.ts
  5. 2 6
      webview-ui/src/__tests__/ContextWindowProgress.spec.tsx
  6. 13 3
      webview-ui/src/components/chat/ChatRow.tsx
  7. 3 1
      webview-ui/src/components/chat/ContextWindowProgress.tsx
  8. 17 3
      webview-ui/src/components/chat/TaskHeader.tsx
  9. 9 0
      webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
  10. 122 104
      webview-ui/src/components/cloud/CloudView.tsx
  11. 183 0
      webview-ui/src/components/cloud/UsagePreview.tsx
  12. 2 1
      webview-ui/src/i18n/locales/ca/chat.json
  13. 11 0
      webview-ui/src/i18n/locales/ca/cloud.json
  14. 2 1
      webview-ui/src/i18n/locales/de/chat.json
  15. 11 0
      webview-ui/src/i18n/locales/de/cloud.json
  16. 2 1
      webview-ui/src/i18n/locales/en/chat.json
  17. 11 0
      webview-ui/src/i18n/locales/en/cloud.json
  18. 2 1
      webview-ui/src/i18n/locales/es/chat.json
  19. 11 0
      webview-ui/src/i18n/locales/es/cloud.json
  20. 2 1
      webview-ui/src/i18n/locales/fr/chat.json
  21. 11 0
      webview-ui/src/i18n/locales/fr/cloud.json
  22. 2 1
      webview-ui/src/i18n/locales/hi/chat.json
  23. 11 0
      webview-ui/src/i18n/locales/hi/cloud.json
  24. 2 1
      webview-ui/src/i18n/locales/id/chat.json
  25. 11 0
      webview-ui/src/i18n/locales/id/cloud.json
  26. 2 1
      webview-ui/src/i18n/locales/it/chat.json
  27. 11 0
      webview-ui/src/i18n/locales/it/cloud.json
  28. 2 1
      webview-ui/src/i18n/locales/ja/chat.json
  29. 11 0
      webview-ui/src/i18n/locales/ja/cloud.json
  30. 2 1
      webview-ui/src/i18n/locales/ko/chat.json
  31. 11 0
      webview-ui/src/i18n/locales/ko/cloud.json
  32. 2 1
      webview-ui/src/i18n/locales/nl/chat.json
  33. 11 0
      webview-ui/src/i18n/locales/nl/cloud.json
  34. 2 1
      webview-ui/src/i18n/locales/pl/chat.json
  35. 11 0
      webview-ui/src/i18n/locales/pl/cloud.json
  36. 2 1
      webview-ui/src/i18n/locales/pt-BR/chat.json
  37. 11 0
      webview-ui/src/i18n/locales/pt-BR/cloud.json
  38. 2 1
      webview-ui/src/i18n/locales/ru/chat.json
  39. 11 0
      webview-ui/src/i18n/locales/ru/cloud.json
  40. 2 1
      webview-ui/src/i18n/locales/tr/chat.json
  41. 11 0
      webview-ui/src/i18n/locales/tr/cloud.json
  42. 2 1
      webview-ui/src/i18n/locales/vi/chat.json
  43. 11 0
      webview-ui/src/i18n/locales/vi/cloud.json
  44. 2 1
      webview-ui/src/i18n/locales/zh-CN/chat.json
  45. 11 0
      webview-ui/src/i18n/locales/zh-CN/cloud.json
  46. 2 1
      webview-ui/src/i18n/locales/zh-TW/chat.json
  47. 11 0
      webview-ui/src/i18n/locales/zh-TW/cloud.json
  48. 13 0
      webview-ui/src/utils/format.ts

+ 29 - 7
packages/cloud/src/CloudAPI.ts

@@ -1,6 +1,13 @@
 import { z } from "zod"
 
-import { type AuthService, type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types"
+import {
+	type AuthService,
+	type ShareVisibility,
+	type ShareResponse,
+	shareResponseSchema,
+	type UsageStats,
+	usageStatsSchema,
+} from "@roo-code/types"
 
 import { getRooCodeApiUrl } from "./config.js"
 import { getUserAgent } from "./utils.js"
@@ -53,9 +60,11 @@ export class CloudAPI {
 			})
 
 			if (!response.ok) {
+				this.log(`[CloudAPI] Request to ${endpoint} failed with status ${response.status}`)
 				await this.handleErrorResponse(response, endpoint)
 			}
 
+			// Log before attempting to read the body
 			const data = await response.json()
 
 			if (parseResponse) {
@@ -86,9 +95,15 @@ export class CloudAPI {
 		let responseBody: unknown
 
 		try {
-			responseBody = await response.json()
-		} catch {
-			responseBody = await response.text()
+			const bodyText = await response.text()
+
+			try {
+				responseBody = JSON.parse(bodyText)
+			} catch {
+				responseBody = bodyText
+			}
+		} catch (_error) {
+			responseBody = "Failed to read error response"
 		}
 
 		switch (response.status) {
@@ -109,15 +124,12 @@ export class CloudAPI {
 	}
 
 	async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise<ShareResponse> {
-		this.log(`[CloudAPI] Sharing task ${taskId} with visibility: ${visibility}`)
-
 		const response = await this.request("/api/extension/share", {
 			method: "POST",
 			body: JSON.stringify({ taskId, visibility }),
 			parseResponse: (data) => shareResponseSchema.parse(data),
 		})
 
-		this.log("[CloudAPI] Share response:", response)
 		return response
 	}
 
@@ -134,4 +146,14 @@ export class CloudAPI {
 					.parse(data),
 		})
 	}
+
+	async getUsagePreview(): Promise<UsageStats> {
+		const response = await this.request("/api/analytics/usage/daily?period=7", {
+			method: "GET",
+			parseResponse: (data) => {
+				return usageStatsSchema.parse(data)
+			},
+		})
+		return response
+	}
 }

+ 63 - 0
src/core/webview/webviewMessageHandler.ts

@@ -3110,5 +3110,68 @@ export const webviewMessageHandler = async (
 			})
 			break
 		}
+		case "getUsagePreview": {
+			try {
+				// Get the CloudAPI instance and fetch usage preview
+				const cloudApi = CloudService.instance.cloudAPI
+				if (!cloudApi) {
+					// User is not authenticated
+					provider.log("[webviewMessageHandler] User not authenticated for usage preview")
+					await provider.postMessageToWebview({
+						type: "usagePreviewData",
+						error: "Authentication required",
+						data: null,
+					})
+					break
+				}
+
+				// Fetch usage preview data
+				const rawUsageData = await cloudApi.getUsagePreview()
+
+				// Transform the data to match UI expectations
+				// The API returns data with separate arrays, but UI expects an array of day objects
+				const dates = rawUsageData.data?.dates ?? []
+				const tasks = rawUsageData.data?.tasks ?? []
+				const tokens = rawUsageData.data?.tokens ?? []
+				const costs = rawUsageData.data?.costs ?? []
+				const len = Math.min(dates.length, tasks.length, tokens.length, costs.length)
+
+				const transformedData = {
+					days: Array.from({ length: len }).map((_, index) => ({
+						date: dates[index] ?? "",
+						taskCount: tasks[index] ?? 0,
+						tokenCount: tokens[index] ?? 0,
+						cost: costs[index] ?? 0,
+					})),
+					totals: rawUsageData.data?.totals || {
+						tasks: 0,
+						tokens: 0,
+						cost: 0,
+					},
+				}
+
+				// Send the transformed data back to the webview
+				await provider.postMessageToWebview({
+					type: "usagePreviewData",
+					data: transformedData,
+					error: undefined,
+				})
+			} catch (error) {
+				provider.log(
+					`[webviewMessageHandler] Failed to fetch usage preview: ${error instanceof Error ? error.message : String(error)}`,
+				)
+				provider.log(
+					`[webviewMessageHandler] Error stack trace: ${error instanceof Error ? error.stack : "No stack trace"}`,
+				)
+
+				// Send error back to webview
+				await provider.postMessageToWebview({
+					type: "usagePreviewData",
+					error: error instanceof Error ? error.message : "Failed to load usage data",
+					data: null,
+				})
+			}
+			break
+		}
 	}
 }

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -126,6 +126,7 @@ export interface ExtensionMessage {
 		| "insertTextIntoTextarea"
 		| "dismissedUpsells"
 		| "organizationSwitchResult"
+		| "usagePreviewData"
 	text?: string
 	payload?: any // Add a generic payload for now, can refine later
 	action?:
@@ -205,6 +206,7 @@ export interface ExtensionMessage {
 	queuedMessages?: QueuedMessage[]
 	list?: string[] // For dismissedUpsells
 	organizationId?: string | null // For organizationSwitchResult
+	data?: any // For usagePreviewData
 }
 
 export type ExtensionState = Pick<

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -229,6 +229,7 @@ export interface WebviewMessage {
 		| "editQueuedMessage"
 		| "dismissUpsell"
 		| "getDismissedUpsells"
+		| "getUsagePreview"
 	text?: string
 	editedMessageContent?: string
 	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

+ 2 - 6
webview-ui/src/__tests__/ContextWindowProgress.spec.tsx

@@ -8,6 +8,7 @@ import TaskHeader from "@src/components/chat/TaskHeader"
 // Mock formatLargeNumber function
 vi.mock("@/utils/format", () => ({
 	formatLargeNumber: vi.fn((num) => num.toString()),
+	formatCost: (cost: number) => `$${cost.toFixed(2)}`,
 }))
 
 // Mock VSCodeBadge component for all tests
@@ -128,12 +129,7 @@ describe("ContextWindowProgress", () => {
 		expect(windowSize).toBeInTheDocument()
 		expect(windowSize).toHaveTextContent("4000")
 
-		// The progress bar is now wrapped in tooltips, but we can verify the structure exists
-		// by checking for the progress bar container
-		const progressBarContainer = screen.getByTestId("context-tokens-count").parentElement
+		const progressBarContainer = screen.getByTestId("context-progress-bar-container").parentElement
 		expect(progressBarContainer).toBeInTheDocument()
-
-		// Verify the flex container has the expected structure
-		expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument()
 	})
 })

+ 13 - 3
webview-ui/src/components/chat/ChatRow.tsx

@@ -132,7 +132,8 @@ export const ChatRowContent = ({
 }: ChatRowContentProps) => {
 	const { t } = useTranslation()
 
-	const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
+	const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, cloudIsAuthenticated } =
+		useExtensionState()
 	const { info: model } = useSelectedModel(apiConfiguration)
 	const [isEditing, setIsEditing] = useState(false)
 	const [editedContent, setEditedContent] = useState("")
@@ -1074,8 +1075,17 @@ export const ChatRowContent = ({
 									{title}
 								</div>
 								<div
-									className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
-									style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
+									className={cn(
+										"text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg",
+										cloudIsAuthenticated &&
+											"cursor-pointer hover:bg-vscode-dropdown-background hover:border-vscode-dropdown-border transition-colors",
+									)}
+									style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}
+									onClick={(e) => {
+										e.stopPropagation() // Prevent parent onClick from firing
+										vscode.postMessage({ type: "switchTab", tab: "cloud" })
+									}}
+									title={t("chat:apiRequest.viewTokenUsage")}>
 									${Number(cost || 0)?.toFixed(4)}
 								</div>
 							</div>

+ 3 - 1
webview-ui/src/components/chat/ContextWindowProgress.tsx

@@ -60,7 +60,9 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
 				<StandardTooltip content={tooltipContent} side="top" sideOffset={8}>
 					<div className="flex-1 relative">
 						{/* Main progress bar container */}
-						<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
+						<div
+							data-testid="context-progress-bar-container"
+							className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
 							{/* Current tokens container */}
 							<div
 								className="relative h-full"

+ 17 - 3
webview-ui/src/components/chat/TaskHeader.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"
 import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
 import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
 import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
-import { FoldVertical, ChevronUp, ChevronDown } from "lucide-react"
+import { FoldVertical, ChevronUp, ChevronDown, ChartColumn } from "lucide-react"
 import prettyBytes from "pretty-bytes"
 
 import type { ClineMessage } from "@roo-code/types"
@@ -11,7 +11,7 @@ import type { ClineMessage } from "@roo-code/types"
 import { getModelMaxOutputTokens } from "@roo/api"
 import { findLastIndex } from "@roo/array"
 
-import { formatLargeNumber } from "@src/utils/format"
+import { formatCost, formatLargeNumber } from "@src/utils/format"
 import { cn } from "@src/lib/utils"
 import { StandardTooltip } from "@src/components/ui"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -24,6 +24,8 @@ import { ContextWindowProgress } from "./ContextWindowProgress"
 import { Mention } from "./Mention"
 import { TodoListDisplay } from "./TodoListDisplay"
 
+import { vscode } from "@src/utils/vscode"
+
 export interface TaskHeaderProps {
 	task: ClineMessage
 	tokensIn: number
@@ -301,7 +303,19 @@ const TaskHeader = ({
 												{t("chat:task.apiCost")}
 											</th>
 											<td className="align-top">
-												<span>${totalCost?.toFixed(2)}</span>
+												<span>{formatCost(totalCost)}</span>
+												<StandardTooltip content={t("chat:apiRequest.viewTokenUsage")}>
+													<ChartColumn
+														className="inline size-3.5 -mt-0.5 ml-2 text-vscode-textLink-foreground cursor-pointer hover:text-vscode-textLink-activeForeground transition-colors"
+														onClick={(e) => {
+															e.stopPropagation()
+															vscode.postMessage({
+																type: "switchTab",
+																tab: "cloud",
+															})
+														}}
+													/>
+												</StandardTooltip>
 											</td>
 										</tr>
 									)}

+ 9 - 0
webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx

@@ -88,6 +88,15 @@ vi.mock("@roo/array", () => ({
 	},
 }))
 
+// Mock the format utilities
+vi.mock("@/utils/format", async (importOriginal) => {
+	const actual = await importOriginal<typeof import("@/utils/format")>()
+	return {
+		...actual,
+		formatCost: (cost: number) => `$${cost.toFixed(2)}`,
+	}
+})
+
 describe("TaskHeader", () => {
 	const defaultProps: TaskHeaderProps = {
 		task: { type: "say", ts: Date.now(), text: "Test task", images: [] },

+ 122 - 104
webview-ui/src/components/cloud/CloudView.tsx

@@ -9,12 +9,13 @@ import { vscode } from "@src/utils/vscode"
 import { telemetryClient } from "@src/utils/TelemetryClient"
 import { ToggleSwitch } from "@/components/ui/toggle-switch"
 import { renderCloudBenefitsContent } from "./CloudUpsellDialog"
-import { CircleAlert, Info, Lock, TriangleAlert } from "lucide-react"
+import { CircleAlert, Lock, TriangleAlert } from "lucide-react"
 import { cn } from "@/lib/utils"
 import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { Button } from "@/components/ui/button"
 import { OrganizationSwitcher } from "./OrganizationSwitcher"
 import { StandardTooltip } from "../ui"
+import { UsagePreview } from "./UsagePreview"
 
 // Define the production URL constant locally to avoid importing from cloud package in tests
 const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
@@ -145,6 +146,14 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone, orga
 		}
 	}
 
+	const handleViewUsageStats = () => {
+		const baseUrl = cloudApiUrl || PRODUCTION_ROO_CODE_API_URL
+		vscode.postMessage({
+			type: "openExternal",
+			url: `${baseUrl}/usage?utm_source=extension&utm_medium=stats_preview&utm_campaign=stats_preview`,
+		})
+	}
+
 	const handleRemoteControlToggle = () => {
 		const newValue = !remoteControlEnabled
 		setRemoteControlEnabled(newValue)
@@ -164,119 +173,128 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone, orga
 				<Button onClick={onDone}>{t("settings:common.done")}</Button>
 			</TabHeader>
 
-			<TabContent className="pt-10">
+			<TabContent className="flex flex-col justify-between min-h-full">
 				{isAuthenticated ? (
-					<>
-						{userInfo && (
-							<div className="flex flex-col items-start ml-4 mb-6">
-								<div className="w-16 h-16 mb-3 rounded-full overflow-hidden">
-									{userInfo?.picture ? (
-										<img
-											src={userInfo.picture}
-											alt={t("cloud:profilePicture")}
-											className="w-full h-full object-cover"
-										/>
-									) : (
-										<div className="w-full h-full flex items-center justify-center bg-vscode-button-background text-vscode-button-foreground text-xl">
-											{userInfo?.name?.charAt(0) || userInfo?.email?.charAt(0) || "?"}
+					<div className="flex flex-col h-full">
+						{/* Content that scrolls */}
+						<div className="">
+							{userInfo && (
+								<div className="flex items-start gap-4 ml-4 mb-6 flex-col min-[300px]:flex-row">
+									{/* Avatar */}
+									<div className="w-12 h-12 rounded-full overflow-hidden flex-shrink-0">
+										{userInfo?.picture ? (
+											<img
+												src={userInfo.picture}
+												alt={t("cloud:profilePicture")}
+												className="w-full h-full object-cover"
+											/>
+										) : (
+											<div className="w-full h-full flex items-center justify-center bg-vscode-button-background text-vscode-button-foreground text-xl">
+												{userInfo?.name?.charAt(0) || userInfo?.email?.charAt(0) || "?"}
+											</div>
+										)}
+									</div>
+
+									{/* Name, email and org switcher */}
+									<div className="flex flex-col">
+										{userInfo.name && (
+											<h2 className="text-lg font-medium text-vscode-foreground my-0">
+												{userInfo.name}
+											</h2>
+										)}
+										{userInfo?.email && (
+											<p className="text-sm text-vscode-descriptionForeground my-0 mb-2">
+												{userInfo?.email}
+											</p>
+										)}
+
+										{/* Organization Switcher */}
+										<div className="max-w-60">
+											<OrganizationSwitcher
+												userInfo={userInfo}
+												organizations={organizations}
+												cloudApiUrl={cloudApiUrl}
+											/>
 										</div>
-									)}
+									</div>
 								</div>
-								{userInfo.name && (
-									<h2 className="text-lg font-medium text-vscode-foreground my-0">{userInfo.name}</h2>
-								)}
-								{userInfo?.email && (
-									<p className="text-sm text-vscode-descriptionForeground my-0">{userInfo?.email}</p>
-								)}
+							)}
 
-								{/* Organization Switcher - moved below email */}
-								<div className="w-full max-w-60 mt-4">
-									<OrganizationSwitcher
-										userInfo={userInfo}
-										organizations={organizations}
-										cloudApiUrl={cloudApiUrl}
+							{/* Task Sync Toggle - Always shown when authenticated */}
+							<div className="mt-4 p-4 border-b border-t border-vscode-panel-border pl-4 max-w-140">
+								<div className="flex items-center gap-3">
+									<ToggleSwitch
+										checked={taskSyncEnabled}
+										onChange={handleTaskSyncToggle}
+										size="medium"
+										aria-label={t("cloud:taskSync")}
+										data-testid="task-sync-toggle"
+										disabled={!!userInfo?.organizationId}
 									/>
+									<span className="font-medium text-vscode-foreground flex items-center">
+										{t("cloud:taskSync")}
+										{userInfo?.organizationId && (
+											<StandardTooltip content={t("cloud:taskSyncManagedByOrganization")}>
+												<div className="bg-vscode-badge-background text-vscode-badge-foreground/80 p-1.5 ml-2 -mb-2 relative -top-1 rounded-full inline-block cursor-help">
+													<Lock className="size-3 block" />
+												</div>
+											</StandardTooltip>
+										)}
+									</span>
+								</div>
+								<div className="text-vscode-descriptionForeground text-sm mt-1 ml-8">
+									{t("cloud:taskSyncDescription")}
 								</div>
-							</div>
-						)}
-
-						{/* Task Sync Toggle - Always shown when authenticated */}
-						<div className="mt-4 p-4 border-b border-t border-vscode-widget-border pl-4 max-w-140">
-							<div className="flex items-center gap-3 mb-2">
-								<ToggleSwitch
-									checked={taskSyncEnabled}
-									onChange={handleTaskSyncToggle}
-									size="medium"
-									aria-label={t("cloud:taskSync")}
-									data-testid="task-sync-toggle"
-									disabled={!!userInfo?.organizationId}
-								/>
-								<span className="font-medium text-vscode-foreground flex items-center">
-									{t("cloud:taskSync")}
-									{userInfo?.organizationId && (
-										<StandardTooltip content={t("cloud:taskSyncManagedByOrganization")}>
-											<div className="bg-vscode-badge-background text-vscode-badge-foreground/80 p-1.5 ml-2 -mb-2 relative -top-1 rounded-full inline-block cursor-help">
-												<Lock className="size-3 block" />
-											</div>
-										</StandardTooltip>
-									)}
-								</span>
-							</div>
-							<div className="text-vscode-descriptionForeground text-sm mt-1 ml-8">
-								{t("cloud:taskSyncDescription")}
-							</div>
 
-							{/* Remote Control Toggle - Only shown when both extensionBridgeEnabled and featureRoomoteControlEnabled are true */}
-							{userInfo?.extensionBridgeEnabled && featureRoomoteControlEnabled && (
-								<>
-									<div className="flex items-center gap-3 mt-4 mb-2">
-										<ToggleSwitch
-											checked={remoteControlEnabled}
-											onChange={handleRemoteControlToggle}
-											size="medium"
-											aria-label={t("cloud:remoteControl")}
-											data-testid="remote-control-toggle"
-											disabled={!taskSyncEnabled}
-										/>
-										<span className="font-medium text-vscode-foreground">
-											{t("cloud:remoteControl")}
-										</span>
-									</div>
-									<div className="text-vscode-descriptionForeground text-sm mt-1 mb-2 ml-8">
-										{t("cloud:remoteControlDescription")}
-										{!taskSyncEnabled && (
-											<div className="text-vscode-editorWarning-foreground mt-2">
-												<CircleAlert className="inline size-3 mr-1 mb-0.5 text-vscode-editorWarning-foreground" />
-												{t("cloud:remoteControlRequiresTaskSync")}
-											</div>
-										)}
-									</div>
-								</>
-							)}
-						</div>
+								{/* Remote Control Toggle - Only shown when both extensionBridgeEnabled and featureRoomoteControlEnabled are true */}
+								{userInfo?.extensionBridgeEnabled && featureRoomoteControlEnabled && (
+									<>
+										<div className="flex items-center gap-3 mt-4">
+											<ToggleSwitch
+												checked={remoteControlEnabled}
+												onChange={handleRemoteControlToggle}
+												size="medium"
+												aria-label={t("cloud:remoteControl")}
+												data-testid="remote-control-toggle"
+												disabled={!taskSyncEnabled}
+											/>
+											<span className="font-medium text-vscode-foreground">
+												{t("cloud:remoteControl")}
+											</span>
+										</div>
+										<div className="text-vscode-descriptionForeground text-sm mt-1 mb-2 ml-8">
+											{t("cloud:remoteControlDescription")}
+											{!taskSyncEnabled && (
+												<div className="text-vscode-editorWarning-foreground mt-2">
+													<CircleAlert className="inline size-3 mr-1 mb-0.5 text-vscode-editorWarning-foreground" />
+													{t("cloud:remoteControlRequiresTaskSync")}
+												</div>
+											)}
+										</div>
+									</>
+								)}
+							</div>
 
-						<div className="text-vscode-descriptionForeground text-sm mt-4 mb-8 pl-4">
-							<Info className="inline size-3 mr-1 mb-0.5 text-vscode-descriptionForeground" />
-							{t("cloud:usageMetricsAlwaysReported")}
-						</div>
+							{/* Usage Stats Chart Section */}
+							<div className="mt-4 mb-4 px-4">
+								<UsagePreview onViewDetails={handleViewUsageStats} />
+							</div>
 
-						<div className="flex flex-col gap-2 mt-4 pl-4">
-							<VSCodeButton
-								appearance="secondary"
-								onClick={handleVisitCloudWebsite}
-								className="w-full max-w-80">
-								{t("cloud:visitCloudWebsite")}
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="secondary"
-								onClick={handleLogoutClick}
-								className="w-full max-w-80">
-								{t("cloud:logOut")}
-							</VSCodeButton>
+							<div className="flex flex-col gap-2 mt-4 px-4">
+								<VSCodeButton
+									appearance="secondary"
+									onClick={handleVisitCloudWebsite}
+									className="w-full">
+									{t("cloud:visitCloudWebsite")}
+								</VSCodeButton>
+								<VSCodeButton appearance="secondary" onClick={handleLogoutClick} className="w-full">
+									{t("cloud:logOut")}
+								</VSCodeButton>
+							</div>
 						</div>
-					</>
+					</div>
 				) : (
-					<>
+					<div>
 						<div className="flex flex-col items-start gap-4 px-8 max-w-100">
 							<div className={cn(authInProgress && "opacity-50")}>{renderCloudBenefitsContent(t)}</div>
 
@@ -329,7 +347,7 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone, orga
 								</div>
 							)}
 						</div>
-					</>
+					</div>
 				)}
 				{cloudApiUrl && cloudApiUrl !== PRODUCTION_ROO_CODE_API_URL && (
 					<div className="ml-4 mt-6 flex">

+ 183 - 0
webview-ui/src/components/cloud/UsagePreview.tsx

@@ -0,0 +1,183 @@
+import { useEffect, useState } from "react"
+import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
+import { CircleAlert, SquareArrowOutUpRight } from "lucide-react"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { vscode } from "@src/utils/vscode"
+import { formatDateShort, formatLargeNumber, formatCost } from "@/utils/format"
+
+interface DailyUsage {
+	date: string // ISO date string
+	taskCount: number
+	tokenCount: number
+	cost: number // in USD
+}
+
+interface UsageStats {
+	days: DailyUsage[]
+	totals: {
+		tasks: number
+		tokens: number
+		cost: number
+	}
+}
+
+interface UsagePreviewProps {
+	onViewDetails: () => void
+}
+
+export const UsagePreview = ({ onViewDetails }: UsagePreviewProps) => {
+	const { t } = useAppTranslation()
+	const [isLoading, setIsLoading] = useState(true)
+	const [error, setError] = useState<string | null>(null)
+	const [data, setData] = useState<UsageStats | null>(null)
+
+	// Fetch usage data on mount
+	useEffect(() => {
+		setIsLoading(true)
+		setError(null)
+
+		// Request usage preview data from the extension
+		vscode.postMessage({ type: "getUsagePreview" })
+
+		// Listen for the response
+		let timeoutId: ReturnType<typeof setTimeout> | null = null
+		const handleMessage = (event: MessageEvent) => {
+			const message = event.data
+
+			if (message.type === "usagePreviewData") {
+				// Clear timeout on success/error to avoid stale timeout flipping UI into error
+				if (timeoutId) {
+					clearTimeout(timeoutId)
+				}
+
+				if (message.error) {
+					setError(message.error)
+				} else if (message.data) {
+					// Validate the data structure
+					if (!message.data.days || !Array.isArray(message.data.days)) {
+						setError(t("cloud:usagePreview.invalidDataFormat"))
+					} else {
+						setData(message.data)
+					}
+				}
+				setIsLoading(false)
+			}
+		}
+
+		window.addEventListener("message", handleMessage)
+
+		// Clean up listener after 10 seconds (timeout)
+		timeoutId = setTimeout(() => {
+			if (isLoading) {
+				setError(t("cloud:usagePreview.failedToLoad"))
+				setIsLoading(false)
+			}
+		}, 10000)
+
+		return () => {
+			if (timeoutId) {
+				clearTimeout(timeoutId)
+			}
+			window.removeEventListener("message", handleMessage)
+		}
+	}, []) // eslint-disable-line react-hooks/exhaustive-deps
+
+	const getBarHeight = (cost: number): number => {
+		if (!data || !data.days || data.days.length === 0) return 1
+		const maxCost = Math.max(...data.days.map((d) => d.cost))
+		if (!Number.isFinite(maxCost) || maxCost <= 0) return 1
+		// Compute percentage first, then round; enforce minimum height for visibility
+		return Math.max(1, Math.round((cost / maxCost) * 100))
+	}
+
+	// Retry loading
+	const handleRetry = () => {
+		setError(null)
+		setIsLoading(true)
+		vscode.postMessage({ type: "getUsagePreview" })
+	}
+
+	// Loading state
+	if (isLoading) {
+		return (
+			<div
+				className="cursor-pointer group rounded-lg bg-vscode-editor-background hover:bg-vscode-list-hoverBackground transition-colors relative"
+				onClick={onViewDetails}>
+				<div className="p-4">
+					{/* Loading spinner centered in chart area */}
+					<div className="h-20 flex items-center justify-center mb-3">
+						<VSCodeProgressRing className="size-6" />
+					</div>
+				</div>
+			</div>
+		)
+	}
+
+	// Error state
+	if (error || !data) {
+		return (
+			<div
+				className="cursor-pointer group rounded-lg bg-vscode-editor-background hover:bg-vscode-list-hoverBackground transition-colors relative"
+				onClick={handleRetry}>
+				<div className="p-4">
+					{/* Error message in chart area */}
+					<div className="mb-3 text-vscode-descriptionForeground">
+						<CircleAlert className="size-4 mb-2 text-vscode-muted-foreground" />
+						<p className="text-xs font-mono font-bold">{t("cloud:usagePreview.couldNotLoadChart")}</p>
+						<p className="text-xs font-mono">{error}</p>
+						<p className="text-xs font-medium mt-1">{t("cloud:usagePreview.clickToRetry")}</p>
+					</div>
+				</div>
+			</div>
+		)
+	}
+
+	return (
+		<div className="cursor-pointer group rounded-lg bg-vscode-editor-background relative" onClick={onViewDetails}>
+			<div className="p-4">
+				{/* Chart with daily usage bars */}
+				<div
+					className="h-24 min-[450px]:h-40 rounded mb-3 flex items-end gap-1 pb-2"
+					role="img"
+					aria-label={t("cloud:usagePreview.costPastDays", { count: data.days.length })}>
+					{data &&
+						Array.isArray(data.days) &&
+						data.days.map((day, index) => (
+							<div key={index} className="w-full flex flex-col items-center justify-end h-full">
+								<div
+									className="w-full rounded-t-xs transition-all bg-vscode-button-background"
+									style={{ height: `${getBarHeight(day.cost)}%` }}
+									aria-label={`${formatDateShort(new Date(day.date).getTime())}: ${formatCost(day.cost)}`}
+								/>
+								<span className="text-[9px] h-[1em] hidden min-[300px]:block overflow-clip text-center text-muted-foreground mt-0.5">
+									{formatDateShort(new Date(day.date).getTime())}
+								</span>
+							</div>
+						))}
+				</div>
+
+				{/* Stats text */}
+				<div className="flex flex-col justify-between text-sm min-[400px]:flex-row min-[450px]:items-center">
+					<span className="flex items-center gap-1 text-vscode-descriptionForeground">
+						{t("cloud:usagePreview.costPastDays", { count: data.days.length })}
+					</span>
+					<span className="text-vscode-foreground">
+						{t("cloud:usagePreview.tasks", { count: data.totals.tasks })}
+						<span> · </span>
+						{t("cloud:usagePreview.tokens", { count: formatLargeNumber(data.totals.tokens) })}
+						<span> · </span>
+						{formatCost(data.totals.cost)}
+					</span>
+				</div>
+			</div>
+
+			{/* Hover overlay */}
+			<div className="absolute inset-0 bg-vscode-editor-background/85 rounded-lg flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
+				<div className="flex items-center gap-2 text-vscode-foreground">
+					<span>{t("cloud:usagePreview.seeMoreStats")}</span>
+					<SquareArrowOutUpRight className="size-3" />
+				</div>
+			</div>
+		</div>
+	)
+}

+ 2 - 1
webview-ui/src/i18n/locales/ca/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Sol·licitud API ha fallat",
 		"streaming": "Sol·licitud API...",
 		"cancelled": "Sol·licitud API cancel·lada",
-		"streamingFailed": "Transmissió API ha fallat"
+		"streamingFailed": "Transmissió API ha fallat",
+		"viewTokenUsage": "Veure les estadístiques d'ús de fitxes"
 	},
 	"checkpoint": {
 		"regular": "Punt de control",

+ 11 - 0
webview-ui/src/i18n/locales/ca/cloud.json

@@ -26,6 +26,17 @@
 	"personalAccount": "Compte Personal",
 	"createTeamAccount": "Crear Compte d'Equip",
 	"switchAccount": "Canviar Compte de Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Cost diari dels darrers {{count}} dies",
+		"seeMoreStats": "Consulta més estadístiques a Roo Code Cloud",
+		"invalidDataFormat": "S'ha rebut un format de dades no vàlid",
+		"failedToLoad": "No s'han pogut carregar les dades d'ús",
+		"couldNotLoadChart": "No s'ha pogut carregar el gràfic:",
+		"clickToRetry": "Fes clic per tornar a intentar-ho",
+		"tasks_one": "{{count}} tasca",
+		"tasks_other": "{{count}} tasques",
+		"tokens": "{{count}} tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Donant-li una mica d'independència a Roo? Controla'l des de qualsevol lloc amb Roo Code Cloud. <learnMoreLink>Més informació</learnMoreLink>.",
 		"longRunningTask": "Això pot trigar una estona. Continua des de qualsevol lloc amb Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/de/chat.json

@@ -141,7 +141,8 @@
 		"failed": "API-Anfrage fehlgeschlagen",
 		"streaming": "API-Anfrage...",
 		"cancelled": "API-Anfrage abgebrochen",
-		"streamingFailed": "API-Streaming fehlgeschlagen"
+		"streamingFailed": "API-Streaming fehlgeschlagen",
+		"viewTokenUsage": "Token-Nutzungsstatistiken anzeigen"
 	},
 	"checkpoint": {
 		"regular": "Checkpoint",

+ 11 - 0
webview-ui/src/i18n/locales/de/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Roo Code Cloud Konto wechseln",
 	"createTeamAccount": "Team-Konto erstellen",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "Tägliche Kosten der letzten {{count}} Tage",
+		"seeMoreStats": "Weitere Statistiken in Roo Code Cloud anzeigen",
+		"invalidDataFormat": "Ungültiges Datenformat empfangen",
+		"failedToLoad": "Nutzungsdaten konnten nicht geladen werden",
+		"couldNotLoadChart": "Diagramm konnte nicht geladen werden:",
+		"clickToRetry": "Klicken, um es erneut zu versuchen",
+		"tasks_one": "{{count}} Aufgabe",
+		"tasks_other": "{{count}} Aufgaben",
+		"tokens": "{{count}} Tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Roo etwas Unabhängigkeit geben? Kontrolliere es von überall mit Roo Code Cloud. <learnMoreLink>Mehr erfahren</learnMoreLink>.",
 		"longRunningTask": "Das könnte eine Weile dauern. Mit Cloud von überall weitermachen.",

+ 2 - 1
webview-ui/src/i18n/locales/en/chat.json

@@ -147,7 +147,8 @@
 		"failed": "API Request Failed",
 		"streaming": "API Request...",
 		"cancelled": "API Request Cancelled",
-		"streamingFailed": "API Streaming Failed"
+		"streamingFailed": "API Streaming Failed",
+		"viewTokenUsage": "View token usage stats"
 	},
 	"checkpoint": {
 		"regular": "Checkpoint",

+ 11 - 0
webview-ui/src/i18n/locales/en/cloud.json

@@ -26,6 +26,17 @@
 	"personalAccount": "Personal Account",
 	"switchAccount": "Switch Roo Code Cloud Account",
 	"createTeamAccount": "Create Team Account",
+	"usagePreview": {
+		"costPastDays": "Daily cost past {{count}} days",
+		"seeMoreStats": "See more stats in Roo Code Cloud",
+		"invalidDataFormat": "Invalid data format received",
+		"failedToLoad": "Failed to load usage data",
+		"couldNotLoadChart": "Couldn't load chart:",
+		"clickToRetry": "Click to retry",
+		"tasks_one": "{{count}} task",
+		"tasks_other": "{{count}} tasks",
+		"tokens": "{{count}} tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Giving Roo some independence? Control it from anywhere with Roo Code Cloud. <learnMoreLink>Learn more</learnMoreLink>.",
 		"longRunningTask": "This might take a while. Continue from anywhere with Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/es/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Solicitud API falló",
 		"streaming": "Solicitud API...",
 		"cancelled": "Solicitud API cancelada",
-		"streamingFailed": "Transmisión API falló"
+		"streamingFailed": "Transmisión API falló",
+		"viewTokenUsage": "Ver estadísticas de uso de tokens"
 	},
 	"checkpoint": {
 		"regular": "Punto de control",

+ 11 - 0
webview-ui/src/i18n/locales/es/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Cambiar Cuenta de Roo Code Cloud",
 	"createTeamAccount": "Crear Cuenta de Equipo",
 	"cloudUrlPillLabel": "URL de Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Costo diario de los últimos {{count}} días",
+		"seeMoreStats": "Ver más estadísticas en Roo Code Cloud",
+		"invalidDataFormat": "Formato de datos recibido no válido",
+		"failedToLoad": "No se pudieron cargar los datos de uso",
+		"couldNotLoadChart": "No se pudo cargar el gráfico:",
+		"clickToRetry": "Haz clic para reintentar",
+		"tasks_one": "{{count}} tarea",
+		"tasks_other": "{{count}} tareas",
+		"tokens": "{{count}} tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "¿Dándole a Roo un poco de independencia? Contrólalo desde cualquier lugar con Roo Code Cloud. <learnMoreLink>Saber más</learnMoreLink>.",
 		"longRunningTask": "Esto podría tardar un poco. Continúa desde cualquier lugar con la Nube.",

+ 2 - 1
webview-ui/src/i18n/locales/fr/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Échec de la requête API",
 		"streaming": "Requête API...",
 		"cancelled": "Requête API annulée",
-		"streamingFailed": "Échec du streaming API"
+		"streamingFailed": "Échec du streaming API",
+		"viewTokenUsage": "Voir les statistiques d'utilisation des tokens"
 	},
 	"checkpoint": {
 		"regular": "Point de contrôle",

+ 11 - 0
webview-ui/src/i18n/locales/fr/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Changer de Compte Roo Code Cloud",
 	"createTeamAccount": "Créer un Compte d'Équipe",
 	"cloudUrlPillLabel": "URL de Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Coût quotidien des {{count}} derniers jours",
+		"seeMoreStats": "Voir plus de statistiques dans Roo Code Cloud",
+		"invalidDataFormat": "Format de données reçu invalide",
+		"failedToLoad": "Échec du chargement des données d'utilisation",
+		"couldNotLoadChart": "Impossible de charger le graphique :",
+		"clickToRetry": "Cliquez pour réessayer",
+		"tasks_one": "{{count}} tâche",
+		"tasks_other": "{{count}} tâches",
+		"tokens": "{{count}} jetons"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Donner à Roo un peu d'indépendance ? Contrôlez-le de n'importe où avec Roo Code Cloud. <learnMoreLink>En savoir plus</learnMoreLink>.",
 		"longRunningTask": "Cela peut prendre un certain temps. Continuez de n'importe où avec le Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/hi/chat.json

@@ -141,7 +141,8 @@
 		"failed": "API अनुरोध विफल हुआ",
 		"streaming": "API अनुरोध...",
 		"cancelled": "API अनुरोध रद्द किया गया",
-		"streamingFailed": "API स्ट्रीमिंग विफल हुई"
+		"streamingFailed": "API स्ट्रीमिंग विफल हुई",
+		"viewTokenUsage": "टोकन उपयोग आँकड़े देखें"
 	},
 	"checkpoint": {
 		"regular": "चेकपॉइंट",

+ 11 - 0
webview-ui/src/i18n/locales/hi/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Roo Code Cloud खाता बदलें",
 	"createTeamAccount": "टीम खाता बनाएं",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "पिछले {{count}} दिनों की दैनिक लागत",
+		"seeMoreStats": "रू कोड क्लाउड में और आँकड़े देखें",
+		"invalidDataFormat": "अमान्य डेटा प्रारूप प्राप्त हुआ",
+		"failedToLoad": "उपयोग डेटा लोड करने में विफल",
+		"couldNotLoadChart": "चार्ट लोड नहीं हो सका:",
+		"clickToRetry": "पुनः प्रयास करने के लिए क्लिक करें",
+		"tasks_one": "{{count}} कार्य",
+		"tasks_other": "{{count}} कार्य",
+		"tokens": "{{count}} टोकन"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "रू को थोड़ी स्वतंत्रता दे रहे हैं? रू कोड क्लाउड के साथ इसे कहीं से भी नियंत्रित करें। <learnMoreLink>और जानें</learnMoreLink>।",
 		"longRunningTask": "इसमें थोड़ा समय लग सकता है। क्लाउड के साथ कहीं से भी जारी रखें।",

+ 2 - 1
webview-ui/src/i18n/locales/id/chat.json

@@ -150,7 +150,8 @@
 		"failed": "Permintaan API Gagal",
 		"streaming": "Permintaan API...",
 		"cancelled": "Permintaan API Dibatalkan",
-		"streamingFailed": "Streaming API Gagal"
+		"streamingFailed": "Streaming API Gagal",
+		"viewTokenUsage": "Lihat statistik penggunaan token"
 	},
 	"checkpoint": {
 		"regular": "Checkpoint",

+ 11 - 0
webview-ui/src/i18n/locales/id/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Ganti Akun Roo Code Cloud",
 	"createTeamAccount": "Buat Akun Tim",
 	"cloudUrlPillLabel": "URL Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Biaya harian {{count}} hari terakhir",
+		"seeMoreStats": "Lihat statistik lainnya di Roo Code Cloud",
+		"invalidDataFormat": "Format data yang diterima tidak valid",
+		"failedToLoad": "Gagal memuat data penggunaan",
+		"couldNotLoadChart": "Tidak dapat memuat bagan:",
+		"clickToRetry": "Klik untuk mencoba lagi",
+		"tasks_one": "{{count}} tugas",
+		"tasks_other": "{{count}} tugas",
+		"tokens": "{{count}} token"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Memberi Roo sedikit kebebasan? Kendalikan dari mana saja dengan Roo Code Cloud. <learnMoreLink>Pelajari lebih lanjut</learnMoreLink>.",
 		"longRunningTask": "Ini mungkin akan memakan waktu cukup lama. Lanjutkan dari mana saja dengan Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/it/chat.json

@@ -144,7 +144,8 @@
 		"failed": "Richiesta API fallita",
 		"streaming": "Richiesta API...",
 		"cancelled": "Richiesta API annullata",
-		"streamingFailed": "Streaming API fallito"
+		"streamingFailed": "Streaming API fallito",
+		"viewTokenUsage": "Visualizza le statistiche di utilizzo dei token"
 	},
 	"checkpoint": {
 		"regular": "Checkpoint",

+ 11 - 0
webview-ui/src/i18n/locales/it/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Cambia Account Roo Code Cloud",
 	"createTeamAccount": "Crea Account del Team",
 	"cloudUrlPillLabel": "URL di Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Costo giornaliero degli ultimi {{count}} giorni",
+		"seeMoreStats": "Vedi altre statistiche in Roo Code Cloud",
+		"invalidDataFormat": "Formato dati ricevuto non valido",
+		"failedToLoad": "Impossibile caricare i dati di utilizzo",
+		"couldNotLoadChart": "Impossibile caricare il grafico:",
+		"clickToRetry": "Clicca per riprovare",
+		"tasks_one": "{{count}} attività",
+		"tasks_other": "{{count}} attività",
+		"tokens": "{{count}} token"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Vuoi dare un po' di indipendenza a Roo? Controllalo da qualsiasi luogo con Roo Code Cloud. <learnMoreLink>Scopri di più</learnMoreLink>.",
 		"longRunningTask": "Potrebbe volerci un po' di tempo. Continua da qualsiasi luogo con il Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/ja/chat.json

@@ -141,7 +141,8 @@
 		"failed": "APIリクエスト失敗",
 		"streaming": "APIリクエスト...",
 		"cancelled": "APIリクエストキャンセル",
-		"streamingFailed": "APIストリーミング失敗"
+		"streamingFailed": "APIストリーミング失敗",
+		"viewTokenUsage": "トークン使用状況の統計を表示"
 	},
 	"checkpoint": {
 		"regular": "チェックポイント",

+ 11 - 0
webview-ui/src/i18n/locales/ja/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Roo Code Cloud アカウントを切り替え",
 	"createTeamAccount": "チームアカウントを作成",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "過去{{count}}日間の日次コスト",
+		"seeMoreStats": "Roo Code Cloudでさらに統計を見る",
+		"invalidDataFormat": "無効なデータ形式を受信しました",
+		"failedToLoad": "使用状況データの読み込みに失敗しました",
+		"couldNotLoadChart": "チャートを読み込めませんでした:",
+		"clickToRetry": "クリックして再試行",
+		"tasks_one": "{{count}}タスク",
+		"tasks_other": "{{count}}タスク",
+		"tokens": "{{count}}トークン"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Rooに少し独立性を与えませんか?Roo Code Cloudでどこからでもコントロールできます。<learnMoreLink>詳細</learnMoreLink>。",
 		"longRunningTask": "これには時間がかかるかもしれません。Cloudを使えばどこからでも続けられます。",

+ 2 - 1
webview-ui/src/i18n/locales/ko/chat.json

@@ -141,7 +141,8 @@
 		"failed": "API 요청 실패",
 		"streaming": "API 요청...",
 		"cancelled": "API 요청 취소됨",
-		"streamingFailed": "API 스트리밍 실패"
+		"streamingFailed": "API 스트리밍 실패",
+		"viewTokenUsage": "토큰 사용 통계 보기"
 	},
 	"checkpoint": {
 		"regular": "체크포인트",

+ 11 - 0
webview-ui/src/i18n/locales/ko/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Roo Code Cloud 계정 전환",
 	"createTeamAccount": "팀 계정 만들기",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "지난 {{count}}일간의 일일 비용",
+		"seeMoreStats": "Roo Code Cloud에서 더 많은 통계 보기",
+		"invalidDataFormat": "잘못된 데이터 형식을 받았습니다",
+		"failedToLoad": "사용 데이터를 로드하지 못했습니다",
+		"couldNotLoadChart": "차트를 로드할 수 없습니다:",
+		"clickToRetry": "클릭하여 다시 시도",
+		"tasks_one": "{{count}}개 작업",
+		"tasks_other": "{{count}}개 작업",
+		"tokens": "{{count}}개 토큰"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Roo에게 약간의 독립성을 부여하시겠습니까? Roo Code Cloud로 어디서든 제어하세요. <learnMoreLink>더 알아보기</learnMoreLink>.",
 		"longRunningTask": "시간이 좀 걸릴 수 있습니다. Cloud로 어디서든 계속하세요.",

+ 2 - 1
webview-ui/src/i18n/locales/nl/chat.json

@@ -136,7 +136,8 @@
 		"failed": "API-verzoek mislukt",
 		"streaming": "API-verzoek...",
 		"cancelled": "API-verzoek geannuleerd",
-		"streamingFailed": "API-streaming mislukt"
+		"streamingFailed": "API-streaming mislukt",
+		"viewTokenUsage": "Bekijk tokengebruikstatistieken"
 	},
 	"checkpoint": {
 		"regular": "Checkpoint",

+ 11 - 0
webview-ui/src/i18n/locales/nl/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Wissel van Roo Code Cloud Account",
 	"createTeamAccount": "Teamaccount aanmaken",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "Dagelijkse kosten afgelopen {{count}} dagen",
+		"seeMoreStats": "Bekijk meer statistieken in Roo Code Cloud",
+		"invalidDataFormat": "Ongeldig dataformaat ontvangen",
+		"failedToLoad": "Laden van gebruiksgegevens mislukt",
+		"couldNotLoadChart": "Grafiek kon niet geladen worden:",
+		"clickToRetry": "Klik om opnieuw te proberen",
+		"tasks_one": "{{count}} taak",
+		"tasks_other": "{{count}} taken",
+		"tokens": "{{count}} tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Roo wat onafhankelijkheid geven? Bedien het overal met Roo Code Cloud. <learnMoreLink>Meer informatie</learnMoreLink>.",
 		"longRunningTask": "Dit kan even duren. Ga overal verder met de Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/pl/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Zapytanie API nie powiodło się",
 		"streaming": "Zapytanie API...",
 		"cancelled": "Zapytanie API anulowane",
-		"streamingFailed": "Strumieniowanie API nie powiodło się"
+		"streamingFailed": "Strumieniowanie API nie powiodło się",
+		"viewTokenUsage": "Wyświetl statystyki użycia tokenów"
 	},
 	"checkpoint": {
 		"regular": "Punkt kontrolny",

+ 11 - 0
webview-ui/src/i18n/locales/pl/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Przełącz Konto Roo Code Cloud",
 	"createTeamAccount": "Utwórz Konto Zespołu",
 	"cloudUrlPillLabel": "URL Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Dzienny koszt w ciągu ostatnich {{count}} dni",
+		"seeMoreStats": "Zobacz więcej statystyk w Roo Code Cloud",
+		"invalidDataFormat": "Otrzymano nieprawidłowy format danych",
+		"failedToLoad": "Nie udało się załadować danych o użytkowaniu",
+		"couldNotLoadChart": "Nie można załadować wykresu:",
+		"clickToRetry": "Kliknij, aby ponowić próbę",
+		"tasks_one": "{{count}} zadanie",
+		"tasks_other": "{{count}} zadania",
+		"tokens": "{{count}} tokeny"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Dać Roo trochę niezależności? Kontroluj go z dowolnego miejsca dzięki Roo Code Cloud. <learnMoreLink>Dowiedz się więcej</learnMoreLink>.",
 		"longRunningTask": "To może chwilę potrwać. Kontynuuj z dowolnego miejsca dzięki Chmurze.",

+ 2 - 1
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Requisição API falhou",
 		"streaming": "Requisição API...",
 		"cancelled": "Requisição API cancelada",
-		"streamingFailed": "Streaming API falhou"
+		"streamingFailed": "Streaming API falhou",
+		"viewTokenUsage": "Ver estatísticas de uso de token"
 	},
 	"checkpoint": {
 		"regular": "Ponto de verificação",

+ 11 - 0
webview-ui/src/i18n/locales/pt-BR/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Alternar Conta do Roo Code Cloud",
 	"createTeamAccount": "Criar Conta de Equipe",
 	"cloudUrlPillLabel": "URL do Roo Code Cloud ",
+	"usagePreview": {
+		"costPastDays": "Custo diário dos últimos {{count}} dias",
+		"seeMoreStats": "Veja mais estatísticas no Roo Code Cloud",
+		"invalidDataFormat": "Formato de dados recebido inválido",
+		"failedToLoad": "Falha ao carregar dados de uso",
+		"couldNotLoadChart": "Não foi possível carregar o gráfico:",
+		"clickToRetry": "Clique para tentar novamente",
+		"tasks_one": "{{count}} tarefa",
+		"tasks_other": "{{count}} tarefas",
+		"tokens": "{{count}} tokens"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Dando um pouco de independência ao Roo? Controle-o de qualquer lugar com o Roo Code Cloud. <learnMoreLink>Saiba mais</learnMoreLink>.",
 		"longRunningTask": "Isso pode levar um tempo. Continue de qualquer lugar com a Nuvem.",

+ 2 - 1
webview-ui/src/i18n/locales/ru/chat.json

@@ -136,7 +136,8 @@
 		"failed": "API-запрос не выполнен",
 		"streaming": "API-запрос...",
 		"cancelled": "API-запрос отменен",
-		"streamingFailed": "Ошибка потокового API-запроса"
+		"streamingFailed": "Ошибка потокового API-запроса",
+		"viewTokenUsage": "Посмотреть статистику использования токенов"
 	},
 	"checkpoint": {
 		"regular": "Точка сохранения",

+ 11 - 0
webview-ui/src/i18n/locales/ru/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Переключить аккаунт Roo Code Cloud",
 	"createTeamAccount": "Создать командный аккаунт",
 	"cloudUrlPillLabel": "URL Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Ежедневная стоимость за последние {{count}} дней",
+		"seeMoreStats": "Смотреть больше статистики в Roo Code Cloud",
+		"invalidDataFormat": "Получен неверный формат данных",
+		"failedToLoad": "Не удалось загрузить данные об использовании",
+		"couldNotLoadChart": "Не удалось загрузить диаграмму:",
+		"clickToRetry": "Нажмите, чтобы повторить попытку",
+		"tasks_one": "{{count}} задача",
+		"tasks_other": "{{count}} задачи",
+		"tokens": "{{count}} токенов"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Предоставить Roo немного независимости? Управляйте им из любого места с помощью Roo Code Cloud. <learnMoreLink>Узнать больше</learnMoreLink>.",
 		"longRunningTask": "Это может занять некоторое время. Продолжайте из любого места с помощью Облака.",

+ 2 - 1
webview-ui/src/i18n/locales/tr/chat.json

@@ -141,7 +141,8 @@
 		"failed": "API İsteği Başarısız",
 		"streaming": "API İsteği...",
 		"cancelled": "API İsteği İptal Edildi",
-		"streamingFailed": "API Akışı Başarısız"
+		"streamingFailed": "API Akışı Başarısız",
+		"viewTokenUsage": "Token kullanım istatistiklerini görüntüle"
 	},
 	"checkpoint": {
 		"regular": "Kontrol Noktası",

+ 11 - 0
webview-ui/src/i18n/locales/tr/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Roo Code Cloud Hesabını Değiştir",
 	"createTeamAccount": "Takım Hesabı Oluştur",
 	"cloudUrlPillLabel": "Roo Code Cloud URL'si",
+	"usagePreview": {
+		"costPastDays": "Son {{count}} günün günlük maliyeti",
+		"seeMoreStats": "Roo Code Cloud'da daha fazla istatistik görün",
+		"invalidDataFormat": "Geçersiz veri formatı alındı",
+		"failedToLoad": "Kullanım verileri yüklenemedi",
+		"couldNotLoadChart": "Grafik yüklenemedi:",
+		"clickToRetry": "Yeniden denemek için tıklayın",
+		"tasks_one": "{{count}} görev",
+		"tasks_other": "{{count}} görev",
+		"tokens": "{{count}} token"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Roo'ya biraz bağımsızlık mı veriyorsunuz? Roo Code Cloud ile onu her yerden kontrol edin. <learnMoreLink>Daha fazla bilgi edinin</learnMoreLink>.",
 		"longRunningTask": "Bu biraz zaman alabilir. Bulut ile her yerden devam edin.",

+ 2 - 1
webview-ui/src/i18n/locales/vi/chat.json

@@ -141,7 +141,8 @@
 		"failed": "Yêu cầu API thất bại",
 		"streaming": "Yêu cầu API...",
 		"cancelled": "Yêu cầu API đã hủy",
-		"streamingFailed": "Streaming API thất bại"
+		"streamingFailed": "Streaming API thất bại",
+		"viewTokenUsage": "Xem thống kê sử dụng token"
 	},
 	"checkpoint": {
 		"regular": "Điểm kiểm tra",

+ 11 - 0
webview-ui/src/i18n/locales/vi/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "Chuyển Tài Khoản Roo Code Cloud",
 	"createTeamAccount": "Tạo Tài Khoản Nhóm",
 	"cloudUrlPillLabel": "URL Roo Code Cloud",
+	"usagePreview": {
+		"costPastDays": "Chi phí hàng ngày trong {{count}} ngày qua",
+		"seeMoreStats": "Xem thêm số liệu thống kê trong Roo Code Cloud",
+		"invalidDataFormat": "Định dạng dữ liệu nhận được không hợp lệ",
+		"failedToLoad": "Không thể tải dữ liệu sử dụng",
+		"couldNotLoadChart": "Không thể tải biểu đồ:",
+		"clickToRetry": "Nhấp để thử lại",
+		"tasks_one": "{{count}} nhiệm vụ",
+		"tasks_other": "{{count}} nhiệm vụ",
+		"tokens": "{{count}} token"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "Trao cho Roo một chút độc lập? Kiểm soát nó từ mọi nơi với Roo Code Cloud. <learnMoreLink>Tìm hiểu thêm</learnMoreLink>.",
 		"longRunningTask": "Việc này có thể mất một lúc. Tiếp tục từ mọi nơi với Cloud.",

+ 2 - 1
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -141,7 +141,8 @@
 		"failed": "API请求失败",
 		"streaming": "API请求...",
 		"cancelled": "API请求已取消",
-		"streamingFailed": "API流式传输失败"
+		"streamingFailed": "API流式传输失败",
+		"viewTokenUsage": "查看 Token 使用情况统计"
 	},
 	"checkpoint": {
 		"regular": "检查点",

+ 11 - 0
webview-ui/src/i18n/locales/zh-CN/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "切换 Roo Code Cloud 账户",
 	"createTeamAccount": "创建团队账户",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "过去 {{count}} 天的每日费用",
+		"seeMoreStats": "在 Roo Code Cloud 中查看更多统计信息",
+		"invalidDataFormat": "收到的数据格式无效",
+		"failedToLoad": "无法加载使用数据",
+		"couldNotLoadChart": "无法加载图表:",
+		"clickToRetry": "点击重试",
+		"tasks_one": "{{count}} 个任务",
+		"tasks_other": "{{count}} 个任务",
+		"tokens": "{{count}} 令牌"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "给 Roo 一些独立性?使用 Roo Code Cloud 从任何地方控制它。 <learnMoreLink>了解更多</learnMoreLink>。",
 		"longRunningTask": "这可能需要一段时间。使用 Cloud 从任何地方继续。",

+ 2 - 1
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -147,7 +147,8 @@
 		"failed": "API 請求失敗",
 		"streaming": "正在處理 API 請求...",
 		"cancelled": "API 請求已取消",
-		"streamingFailed": "API 串流處理失敗"
+		"streamingFailed": "API 串流處理失敗",
+		"viewTokenUsage": "檢視權杖使用情況統計"
 	},
 	"checkpoint": {
 		"regular": "檢查點",

+ 11 - 0
webview-ui/src/i18n/locales/zh-TW/cloud.json

@@ -26,6 +26,17 @@
 	"switchAccount": "切換 Roo Code Cloud 帳戶",
 	"createTeamAccount": "創建團隊帳戶",
 	"cloudUrlPillLabel": "Roo Code Cloud URL",
+	"usagePreview": {
+		"costPastDays": "過去 {{count}} 天的每日費用",
+		"seeMoreStats": "在 Roo Code Cloud 中查看更多統計訊息",
+		"invalidDataFormat": "收到的資料格式無效",
+		"failedToLoad": "無法載入使用資料",
+		"couldNotLoadChart": "無法載入圖表:",
+		"clickToRetry": "點擊重試",
+		"tasks_one": "{{count}} 個任務",
+		"tasks_other": "{{count}} 個任務",
+		"tokens": "{{count}} 個 token"
+	},
 	"upsell": {
 		"autoApprovePowerUser": "給 Roo 一點獨立性?使用 Roo Code Cloud 隨時隨地控制它。<learnMoreLink>了解更多</learnMoreLink>。",
 		"longRunningTask": "這可能需要一些時間。使用雲端隨時隨地繼續。",

+ 13 - 0
webview-ui/src/utils/format.ts

@@ -26,6 +26,15 @@ export const formatDate = (timestamp: number) => {
 	})
 }
 
+export const formatDateShort = (timestamp: number) => {
+	const date = new Date(timestamp)
+	const locale = i18next.language || "en"
+	return date.toLocaleDateString(locale, {
+		month: "numeric",
+		day: "numeric",
+	})
+}
+
 export const formatTimeAgo = (timestamp: number) => {
 	const now = Date.now()
 	const diff = now - timestamp
@@ -73,3 +82,7 @@ export const formatTimeAgo = (timestamp: number) => {
 
 	return i18next.t("common:time_ago.just_now")
 }
+
+export const formatCost = (cost: number): string => {
+	return `$${cost.toFixed(2)}`
+}