Skip to content

Commit cd8036d

Browse files
brunobergherroomotegithub-actions[bot]mrubensroomote[bot]
authored
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]>
1 parent 5a3f911 commit cd8036d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+691
-142
lines changed

packages/cloud/src/CloudAPI.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { z } from "zod"
22

3-
import { type AuthService, type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types"
3+
import {
4+
type AuthService,
5+
type ShareVisibility,
6+
type ShareResponse,
7+
shareResponseSchema,
8+
type UsageStats,
9+
usageStatsSchema,
10+
} from "@roo-code/types"
411

512
import { getRooCodeApiUrl } from "./config.js"
613
import { getUserAgent } from "./utils.js"
@@ -53,9 +60,11 @@ export class CloudAPI {
5360
})
5461

5562
if (!response.ok) {
63+
this.log(`[CloudAPI] Request to ${endpoint} failed with status ${response.status}`)
5664
await this.handleErrorResponse(response, endpoint)
5765
}
5866

67+
// Log before attempting to read the body
5968
const data = await response.json()
6069

6170
if (parseResponse) {
@@ -86,9 +95,15 @@ export class CloudAPI {
8695
let responseBody: unknown
8796

8897
try {
89-
responseBody = await response.json()
90-
} catch {
91-
responseBody = await response.text()
98+
const bodyText = await response.text()
99+
100+
try {
101+
responseBody = JSON.parse(bodyText)
102+
} catch {
103+
responseBody = bodyText
104+
}
105+
} catch (_error) {
106+
responseBody = "Failed to read error response"
92107
}
93108

94109
switch (response.status) {
@@ -109,15 +124,12 @@ export class CloudAPI {
109124
}
110125

111126
async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise<ShareResponse> {
112-
this.log(`[CloudAPI] Sharing task ${taskId} with visibility: ${visibility}`)
113-
114127
const response = await this.request("/api/extension/share", {
115128
method: "POST",
116129
body: JSON.stringify({ taskId, visibility }),
117130
parseResponse: (data) => shareResponseSchema.parse(data),
118131
})
119132

120-
this.log("[CloudAPI] Share response:", response)
121133
return response
122134
}
123135

@@ -134,4 +146,14 @@ export class CloudAPI {
134146
.parse(data),
135147
})
136148
}
149+
150+
async getUsagePreview(): Promise<UsageStats> {
151+
const response = await this.request("/api/analytics/usage/daily?period=7", {
152+
method: "GET",
153+
parseResponse: (data) => {
154+
return usageStatsSchema.parse(data)
155+
},
156+
})
157+
return response
158+
}
137159
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3110,5 +3110,68 @@ export const webviewMessageHandler = async (
31103110
})
31113111
break
31123112
}
3113+
case "getUsagePreview": {
3114+
try {
3115+
// Get the CloudAPI instance and fetch usage preview
3116+
const cloudApi = CloudService.instance.cloudAPI
3117+
if (!cloudApi) {
3118+
// User is not authenticated
3119+
provider.log("[webviewMessageHandler] User not authenticated for usage preview")
3120+
await provider.postMessageToWebview({
3121+
type: "usagePreviewData",
3122+
error: "Authentication required",
3123+
data: null,
3124+
})
3125+
break
3126+
}
3127+
3128+
// Fetch usage preview data
3129+
const rawUsageData = await cloudApi.getUsagePreview()
3130+
3131+
// Transform the data to match UI expectations
3132+
// The API returns data with separate arrays, but UI expects an array of day objects
3133+
const dates = rawUsageData.data?.dates ?? []
3134+
const tasks = rawUsageData.data?.tasks ?? []
3135+
const tokens = rawUsageData.data?.tokens ?? []
3136+
const costs = rawUsageData.data?.costs ?? []
3137+
const len = Math.min(dates.length, tasks.length, tokens.length, costs.length)
3138+
3139+
const transformedData = {
3140+
days: Array.from({ length: len }).map((_, index) => ({
3141+
date: dates[index] ?? "",
3142+
taskCount: tasks[index] ?? 0,
3143+
tokenCount: tokens[index] ?? 0,
3144+
cost: costs[index] ?? 0,
3145+
})),
3146+
totals: rawUsageData.data?.totals || {
3147+
tasks: 0,
3148+
tokens: 0,
3149+
cost: 0,
3150+
},
3151+
}
3152+
3153+
// Send the transformed data back to the webview
3154+
await provider.postMessageToWebview({
3155+
type: "usagePreviewData",
3156+
data: transformedData,
3157+
error: undefined,
3158+
})
3159+
} catch (error) {
3160+
provider.log(
3161+
`[webviewMessageHandler] Failed to fetch usage preview: ${error instanceof Error ? error.message : String(error)}`,
3162+
)
3163+
provider.log(
3164+
`[webviewMessageHandler] Error stack trace: ${error instanceof Error ? error.stack : "No stack trace"}`,
3165+
)
3166+
3167+
// Send error back to webview
3168+
await provider.postMessageToWebview({
3169+
type: "usagePreviewData",
3170+
error: error instanceof Error ? error.message : "Failed to load usage data",
3171+
data: null,
3172+
})
3173+
}
3174+
break
3175+
}
31133176
}
31143177
}

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface ExtensionMessage {
126126
| "insertTextIntoTextarea"
127127
| "dismissedUpsells"
128128
| "organizationSwitchResult"
129+
| "usagePreviewData"
129130
text?: string
130131
payload?: any // Add a generic payload for now, can refine later
131132
action?:
@@ -205,6 +206,7 @@ export interface ExtensionMessage {
205206
queuedMessages?: QueuedMessage[]
206207
list?: string[] // For dismissedUpsells
207208
organizationId?: string | null // For organizationSwitchResult
209+
data?: any // For usagePreviewData
208210
}
209211

210212
export type ExtensionState = Pick<

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export interface WebviewMessage {
229229
| "editQueuedMessage"
230230
| "dismissUpsell"
231231
| "getDismissedUpsells"
232+
| "getUsagePreview"
232233
text?: string
233234
editedMessageContent?: string
234235
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

webview-ui/src/__tests__/ContextWindowProgress.spec.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TaskHeader from "@src/components/chat/TaskHeader"
88
// Mock formatLargeNumber function
99
vi.mock("@/utils/format", () => ({
1010
formatLargeNumber: vi.fn((num) => num.toString()),
11+
formatCost: (cost: number) => `$${cost.toFixed(2)}`,
1112
}))
1213

1314
// Mock VSCodeBadge component for all tests
@@ -128,12 +129,7 @@ describe("ContextWindowProgress", () => {
128129
expect(windowSize).toBeInTheDocument()
129130
expect(windowSize).toHaveTextContent("4000")
130131

131-
// The progress bar is now wrapped in tooltips, but we can verify the structure exists
132-
// by checking for the progress bar container
133-
const progressBarContainer = screen.getByTestId("context-tokens-count").parentElement
132+
const progressBarContainer = screen.getByTestId("context-progress-bar-container").parentElement
134133
expect(progressBarContainer).toBeInTheDocument()
135-
136-
// Verify the flex container has the expected structure
137-
expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument()
138134
})
139135
})

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ export const ChatRowContent = ({
132132
}: ChatRowContentProps) => {
133133
const { t } = useTranslation()
134134

135-
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
135+
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, cloudIsAuthenticated } =
136+
useExtensionState()
136137
const { info: model } = useSelectedModel(apiConfiguration)
137138
const [isEditing, setIsEditing] = useState(false)
138139
const [editedContent, setEditedContent] = useState("")
@@ -1074,8 +1075,17 @@ export const ChatRowContent = ({
10741075
{title}
10751076
</div>
10761077
<div
1077-
className="text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg"
1078-
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
1078+
className={cn(
1079+
"text-xs text-vscode-dropdown-foreground border-vscode-dropdown-border/50 border px-1.5 py-0.5 rounded-lg",
1080+
cloudIsAuthenticated &&
1081+
"cursor-pointer hover:bg-vscode-dropdown-background hover:border-vscode-dropdown-border transition-colors",
1082+
)}
1083+
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}
1084+
onClick={(e) => {
1085+
e.stopPropagation() // Prevent parent onClick from firing
1086+
vscode.postMessage({ type: "switchTab", tab: "cloud" })
1087+
}}
1088+
title={t("chat:apiRequest.viewTokenUsage")}>
10791089
${Number(cost || 0)?.toFixed(4)}
10801090
</div>
10811091
</div>

webview-ui/src/components/chat/ContextWindowProgress.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
6060
<StandardTooltip content={tooltipContent} side="top" sideOffset={8}>
6161
<div className="flex-1 relative">
6262
{/* Main progress bar container */}
63-
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
63+
<div
64+
data-testid="context-progress-bar-container"
65+
className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
6466
{/* Current tokens container */}
6567
<div
6668
className="relative h-full"

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { useTranslation } from "react-i18next"
33
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
44
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
55
import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
6-
import { FoldVertical, ChevronUp, ChevronDown } from "lucide-react"
6+
import { FoldVertical, ChevronUp, ChevronDown, ChartColumn } from "lucide-react"
77
import prettyBytes from "pretty-bytes"
88

99
import type { ClineMessage } from "@roo-code/types"
1010

1111
import { getModelMaxOutputTokens } from "@roo/api"
1212
import { findLastIndex } from "@roo/array"
1313

14-
import { formatLargeNumber } from "@src/utils/format"
14+
import { formatCost, formatLargeNumber } from "@src/utils/format"
1515
import { cn } from "@src/lib/utils"
1616
import { StandardTooltip } from "@src/components/ui"
1717
import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -24,6 +24,8 @@ import { ContextWindowProgress } from "./ContextWindowProgress"
2424
import { Mention } from "./Mention"
2525
import { TodoListDisplay } from "./TodoListDisplay"
2626

27+
import { vscode } from "@src/utils/vscode"
28+
2729
export interface TaskHeaderProps {
2830
task: ClineMessage
2931
tokensIn: number
@@ -301,7 +303,19 @@ const TaskHeader = ({
301303
{t("chat:task.apiCost")}
302304
</th>
303305
<td className="align-top">
304-
<span>${totalCost?.toFixed(2)}</span>
306+
<span>{formatCost(totalCost)}</span>
307+
<StandardTooltip content={t("chat:apiRequest.viewTokenUsage")}>
308+
<ChartColumn
309+
className="inline size-3.5 -mt-0.5 ml-2 text-vscode-textLink-foreground cursor-pointer hover:text-vscode-textLink-activeForeground transition-colors"
310+
onClick={(e) => {
311+
e.stopPropagation()
312+
vscode.postMessage({
313+
type: "switchTab",
314+
tab: "cloud",
315+
})
316+
}}
317+
/>
318+
</StandardTooltip>
305319
</td>
306320
</tr>
307321
)}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ vi.mock("@roo/array", () => ({
8888
},
8989
}))
9090

91+
// Mock the format utilities
92+
vi.mock("@/utils/format", async (importOriginal) => {
93+
const actual = await importOriginal<typeof import("@/utils/format")>()
94+
return {
95+
...actual,
96+
formatCost: (cost: number) => `$${cost.toFixed(2)}`,
97+
}
98+
})
99+
91100
describe("TaskHeader", () => {
92101
const defaultProps: TaskHeaderProps = {
93102
task: { type: "say", ts: Date.now(), text: "Test task", images: [] },

0 commit comments

Comments
 (0)