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 <roomote@roocode.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: SannidhyaSah <sah_sannidhya@outlook.com>
Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
This commit is contained in:
Bruno Bergher 2025-10-07 22:48:23 +01:00 committed by GitHub
parent 5a3f911321
commit cd8036d2d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 693 additions and 144 deletions

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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<

View file

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

View file

@ -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()
})
})

View file

@ -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>

View file

@ -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"

View file

@ -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>
)}

View file

@ -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: [] },

View file

@ -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>
)}
</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}
/>
</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 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>
)}
</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>
{/* 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 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>
{/* 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>
{/* 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 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">

View file

@ -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>
)
}

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

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

View file

@ -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": "इसमें थोड़ा समय लग सकता है। क्लाउड के साथ कहीं से भी जारी रखें।",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

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

View file

@ -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を使えばどこからでも続けられます。",

View file

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

View file

@ -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로 어디서든 계속하세요.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

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

View file

@ -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": "Это может занять некоторое время. Продолжайте из любого места с помощью Облака.",

View file

@ -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ı",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

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

View file

@ -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 从任何地方继续。",

View file

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

View file

@ -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": "這可能需要一些時間。使用雲端隨時隨地繼續。",

View file

@ -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)}`
}