Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7b43022
Working chart in CloudView
brunobergher Sep 30, 2025
af0a90e
Lint fixes
brunobergher Sep 30, 2025
2c31616
Actually makes data flow in
brunobergher Sep 30, 2025
d84e031
i18n
brunobergher Sep 30, 2025
eda8cae
Adds utm params
brunobergher Sep 30, 2025
c33cc29
Adds links to CloudView from mentions of cost elsewhere if the user i…
brunobergher Sep 30, 2025
70a2478
Removes requirement of user being authenticated for links to cloud
brunobergher Sep 30, 2025
d1e7192
Adds missing translations
brunobergher Sep 30, 2025
f70062d
fix(cloud): usage preview fixes — clear timeout on response; correct …
roomote Sep 30, 2025
7ab010a
feat: add GLM-4.6 model support for z.ai provider (#8408)
roomote[bot] Sep 30, 2025
b66c5ec
chore: add changeset for v3.28.14 (#8413)
mrubens Sep 30, 2025
e46eb8a
Changeset version bump (#8414)
github-actions[bot] Sep 30, 2025
22113cd
A couple more sonnet 4.5 fixes (#8421)
mrubens Sep 30, 2025
1e287f9
chore: Remove unsupported Gemini 2.5 Flash Image Preview free model (…
SannidhyaSah Sep 30, 2025
bca20e5
Include reasoning messages in cloud tasks (#8401)
mrubens Sep 30, 2025
3ec844a
fix: show send button when only images are selected in chat textarea …
roomote[bot] Sep 30, 2025
3f85776
Add structured data to the homepage (#8427)
mrubens Oct 1, 2025
82e093b
Consolidate formatting functions
brunobergher Oct 1, 2025
09b2fa2
Fixes in TaskHeader
brunobergher Oct 1, 2025
ac7d139
Chinese fix
brunobergher Oct 1, 2025
a989db7
Removes .review directory, no idea what happened here
brunobergher Oct 1, 2025
0d1cd24
Removes tmp directories
brunobergher Oct 1, 2025
6f37b02
Moves types to shared library
brunobergher Oct 1, 2025
61a133d
Test fixes
brunobergher Oct 1, 2025
c13bf8b
Merge remote-tracking branch 'origin/main' into bb/experiment-stats
mrubens Oct 1, 2025
0393a40
Merge remote-tracking branch 'origin/main' into bb/experiment-stats
brunobergher Oct 1, 2025
5b83dd1
Merge branch 'bb/experiment-stats' of github.com:RooCodeInc/Roo-Code …
brunobergher Oct 1, 2025
ab77ff2
Merge remote-tracking branch 'origin/main' into bb/experiment-stats
jr Oct 1, 2025
35154ef
a11y improvements
brunobergher Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions packages/cloud/src/CloudAPI.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}

Expand All @@ -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 changes: 63 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?:
Expand Down Expand Up @@ -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<
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export interface WebviewMessage {
| "editQueuedMessage"
| "dismissUpsell"
| "getDismissedUpsells"
| "getUsagePreview"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down
8 changes: 2 additions & 6 deletions webview-ui/src/__tests__/ContextWindowProgress.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
})
})
16 changes: 13 additions & 3 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down Expand Up @@ -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>
Expand Down
4 changes: 3 additions & 1 deletion webview-ui/src/components/chat/ContextWindowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 17 additions & 3 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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"

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"
Expand All @@ -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
Expand Down Expand Up @@ -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>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] },
Expand Down
Loading