Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 7 additions & 29 deletions packages/cloud/src/CloudAPI.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { z } from "zod"

import {
type AuthService,
type ShareVisibility,
type ShareResponse,
shareResponseSchema,
type UsageStats,
usageStatsSchema,
} from "@roo-code/types"
import { type AuthService, type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types"

import { getRooCodeApiUrl } from "./config.js"
import { getUserAgent } from "./utils.js"
Expand Down Expand Up @@ -60,11 +53,9 @@ 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 @@ -95,15 +86,9 @@ export class CloudAPI {
let responseBody: unknown

try {
const bodyText = await response.text()

try {
responseBody = JSON.parse(bodyText)
} catch {
responseBody = bodyText
}
} catch (_error) {
responseBody = "Failed to read error response"
responseBody = await response.json()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Fallback from response.json() to response.text() can fail because the body stream is single-use. After json() throws due to invalid JSON, the stream is consumed and text() may throw ("body used already"). To robustly capture error bodies, read text once and JSON-parse it, or clone() before consuming. The previous implementation used response.text() then JSON.parse fallback which avoids this pitfall.

} catch {
responseBody = await response.text()
}

switch (response.status) {
Expand All @@ -124,12 +109,15 @@ 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 @@ -146,14 +134,4 @@ 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: 0 additions & 63 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3110,68 +3110,5 @@ 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: 0 additions & 2 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ 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 @@ -206,7 +205,6 @@ 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: 0 additions & 1 deletion src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ export interface WebviewMessage {
| "editQueuedMessage"
| "dismissUpsell"
| "getDismissedUpsells"
| "getUsagePreview"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down
8 changes: 6 additions & 2 deletions webview-ui/src/__tests__/ContextWindowProgress.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ 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 @@ -129,7 +128,12 @@ describe("ContextWindowProgress", () => {
expect(windowSize).toBeInTheDocument()
expect(windowSize).toHaveTextContent("4000")

const progressBarContainer = screen.getByTestId("context-progress-bar-container").parentElement
// 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
expect(progressBarContainer).toBeInTheDocument()

// Verify the flex container has the expected structure
expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument()
})
})
16 changes: 3 additions & 13 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ export const ChatRowContent = ({
}: ChatRowContentProps) => {
const { t } = useTranslation()

const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, cloudIsAuthenticated } =
useExtensionState()
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
Expand Down Expand Up @@ -1075,17 +1074,8 @@ export const ChatRowContent = ({
{title}
</div>
<div
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")}>
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 }}>
${Number(cost || 0)?.toFixed(4)}
</div>
</div>
Expand Down
4 changes: 1 addition & 3 deletions webview-ui/src/components/chat/ContextWindowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
<StandardTooltip content={tooltipContent} side="top" sideOffset={8}>
<div className="flex-1 relative">
{/* Main progress bar container */}
<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)]">
<div 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: 3 additions & 17 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, ChartColumn } from "lucide-react"
import { FoldVertical, ChevronUp, ChevronDown } 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 { formatCost, formatLargeNumber } from "@src/utils/format"
import { 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,8 +24,6 @@ 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 @@ -303,19 +301,7 @@ const TaskHeader = ({
{t("chat:task.apiCost")}
</th>
<td className="align-top">
<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>
<span>${totalCost?.toFixed(2)}</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo/lexical note: The <span> now contains ${totalCost?.toFixed(2)}, which in JSX will be treated as a string rather than an evaluated expression. If the intention is to display the cost with a preceding currency symbol, consider using curly braces to embed the JS expression, for example:

{$${totalCost?.toFixed(2)}}

This change would ensure that the value is properly computed and displayed.

Suggested change
<span>${totalCost?.toFixed(2)}</span>
<span>{`$${totalCost?.toFixed(2)}`}</span>

</td>
</tr>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,6 @@ 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
Loading