Compare commits

...

27 Commits

Author SHA1 Message Date
Frostbourne cc90adb234 Update webview-ui/src/components/account/AccountView.tsx
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-21 18:13:39 -07:00
frostbournesb 8850fbd7f6 Make history table take full height 2025-03-21 18:09:28 -07:00
frostbournesb 5cbe6c8f04 Make account view buttons full width 2025-03-21 17:31:19 -07:00
frostbournesb 3f85941173 Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-21 14:52:21 -07:00
frostbournesb 2428d5aa66 Move shared types to shared folder 2025-03-21 13:40:46 -07:00
frostbournesb ea9e2e1415 Use clineprovider weakref 2025-03-21 13:30:14 -07:00
Saoud Rizwan 49d3566504 Merge branch 'main' into frostbourne/eng-251 2025-03-21 13:24:50 -07:00
frostbournesb fed21d19af fix svg type 2025-03-20 17:16:51 -07:00
frostbournesb 4ab2f517ed Make svg asset a component 2025-03-20 16:26:15 -07:00
frostbournesb e0bf564c8c Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-20 15:53:21 -07:00
Frostbourne c43a9f0b90 Update webview-ui/src/utils/format.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-18 22:20:00 -07:00
frostbournesb 26d9b8ae15 Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-18 22:18:57 -07:00
frostbournesb 210fe0e8c3 linter 2025-03-18 22:18:07 -07:00
frostbournesb 56caf9a6a6 Overflow scroll box 2025-03-18 21:52:32 -07:00
frostbournesb dc1b824207 Properly connect and format data from endpoint 2025-03-18 21:05:53 -07:00
frostbournesb 3aab5b16e7 changeset 2025-03-17 20:48:21 -07:00
frostbournesb 7b9da0c741 prettier 2025-03-17 20:32:01 -07:00
frostbournesb 25246a43f7 refactor creditshistorytable 2025-03-17 20:20:40 -07:00
frostbournesb 64c6944128 Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-17 15:36:00 -07:00
frostbournesb 1a368f121f Get credits data from endpoint 2025-03-17 15:34:54 -07:00
frostbournesb 5f2e55d859 Move AccountInfoCard to tailwind and fix flex styling 2025-03-14 21:05:50 -07:00
frostbournesb 7f24a30bf5 Fix account card in other views 2025-03-14 20:48:01 -07:00
frostbournesb 2a473471e4 Fix flex styling 2025-03-13 19:54:31 -07:00
frostbournesb 27817bcb1a Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-13 10:59:47 -07:00
frostbournesb fb7e365b5b fix dividers 2025-03-12 20:47:44 -07:00
frostbournesb 99c0ce1aa8 Merge remote-tracking branch 'origin/main' into frostbourne/eng-251 2025-03-12 20:15:08 -07:00
frostbournesb 1c0f6887bb Accounts Modal mocked 2025-03-12 20:14:51 -07:00
18 changed files with 555 additions and 137 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added Cline Account panel in extension for easier credits management
+11 -1
View File
@@ -85,6 +85,11 @@
"title": "Open in Editor",
"icon": "$(link-external)"
},
{
"command": "cline.accountButtonClicked",
"title": "Account",
"icon": "$(account)"
},
{
"command": "cline.settingsButtonClicked",
"title": "Settings",
@@ -119,9 +124,14 @@
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.settingsButtonClicked",
"command": "cline.accountButtonClicked",
"group": "navigation@5",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.settingsButtonClicked",
"group": "navigation@6",
"when": "view == claude-dev.SidebarProvider"
}
]
},
+26
View File
@@ -14,6 +14,7 @@ import { fetchOpenGraphData, isImageUrl } from "../../integrations/misc/link-pre
import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "../../services/account/ClineAccountService"
import { McpHub } from "../../services/mcp/McpHub"
import { UserInfo } from "../../shared/UserInfo"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
@@ -122,6 +123,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
accountService?: ClineAccountService
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
conversationTelemetryService: ConversationTelemetryService
@@ -133,6 +135,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
ClineProvider.activeInstances.add(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.mcpHub = new McpHub(this)
this.accountService = new ClineAccountService(this)
this.conversationTelemetryService = new ConversationTelemetryService(this)
// Clean up legacy checkpoints
@@ -164,6 +167,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.workspaceTracker = undefined
this.mcpHub?.dispose()
this.mcpHub = undefined
this.accountService = undefined
this.conversationTelemetryService.shutdown()
this.outputChannel.appendLine("Disposed all disposables")
ClineProvider.activeInstances.delete(this)
@@ -725,6 +729,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.handleSignOut()
break
}
case "showAccountViewClicked": {
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
break
}
case "fetchUserCreditsData": {
await this.fetchUserCreditsData()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
break
@@ -1314,6 +1326,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
// Account
async fetchUserCreditsData() {
try {
await Promise.all([
this.accountService?.fetchBalance(),
this.accountService?.fetchUsageTransactions(),
this.accountService?.fetchPaymentTransactions(),
])
} catch (error) {
console.error("Failed to fetch user credits data:", error)
}
}
// Auth
public async validateAuthState(state: string | null): Promise<boolean> {
+2 -2
View File
@@ -115,10 +115,10 @@ export function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountLoginClicked", () => {
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
sidebarProvider.postMessageToWebview({
type: "action",
action: "accountLoginClicked",
action: "accountButtonClicked",
})
}),
)
+118
View File
@@ -0,0 +1,118 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { ClineProvider } from "../../core/webview/ClineProvider"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private providerRef: WeakRef<ClineProvider>
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.providerRef.deref()
if (!provider) {
return undefined
}
const { apiConfiguration } = await provider.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
}
/**
* Helper function to make authenticated requests to the Cline API
* @param endpoint The API endpoint to call (without the base URL)
* @param config Additional axios request configuration
* @returns The API response data
* @throws Error if the API key is not found or the request fails
*/
private async authenticatedRequest<T>(endpoint: string, config: AxiosRequestConfig = {}): Promise<T> {
const clineApiKey = await this.getClineApiKey()
if (!clineApiKey) {
throw new Error("Cline API key not found")
}
const url = `${this.baseUrl}${endpoint}`
const requestConfig: AxiosRequestConfig = {
...config,
headers: {
Authorization: `Bearer ${clineApiKey}`,
"Content-Type": "application/json",
...config.headers,
},
}
const response: AxiosResponse<T> = await axios.get(url, requestConfig)
if (!response.data) {
throw new Error(`Invalid response from ${endpoint} API`)
}
return response.data
}
/**
* Fetches the user's current credit balance
*/
async fetchBalance(): Promise<BalanceResponse | undefined> {
try {
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsBalance",
userCreditsBalance: data,
})
return data
} catch (error) {
console.error("Failed to fetch balance:", error)
return undefined
}
}
/**
* Fetches the user's usage transactions
*/
async fetchUsageTransactions(): Promise<UsageTransaction[] | undefined> {
try {
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsUsage",
userCreditsUsage: data,
})
return data
} catch (error) {
console.error("Failed to fetch usage transactions:", error)
return undefined
}
}
/**
* Fetches the user's payment transactions
*/
async fetchPaymentTransactions(): Promise<PaymentTransaction[] | undefined> {
try {
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
return data
} catch (error) {
console.error("Failed to fetch payment transactions:", error)
return undefined
}
}
}
+18
View File
@@ -0,0 +1,18 @@
export interface BalanceResponse {
currentBalance: number
}
export interface UsageTransaction {
spentAt: string
credits: string
modelProvider: string
model: string
promptTokens: string
completionTokens: string
}
export interface PaymentTransaction {
paidAt: string
amountCents: string
credits: string
}
+8
View File
@@ -8,6 +8,7 @@ import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
import { TelemetrySetting } from "./TelemetrySetting"
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
// webview will hold state
export interface ExtensionMessage {
@@ -34,6 +35,9 @@ export interface ExtensionMessage {
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
| "totalTasksSize"
text?: string
action?:
@@ -44,6 +48,7 @@ export interface ExtensionMessage {
| "didBecomeVisible"
| "accountLoginClicked"
| "accountLogoutClicked"
| "accountButtonClicked"
invoke?: Invoke
state?: ExtensionState
images?: string[]
@@ -70,6 +75,9 @@ export interface ExtensionMessage {
}
url?: string
isImage?: boolean
userCreditsBalance?: BalanceResponse
userCreditsUsage?: UsageTransaction[]
userCreditsPayments?: PaymentTransaction[]
totalTasksSize?: number | null
}
+2
View File
@@ -45,6 +45,7 @@ export interface WebviewMessage {
| "getLatestState"
| "accountLoginClicked"
| "accountLogoutClicked"
| "showAccountViewClicked"
| "authStateChanged"
| "authCallback"
| "fetchMcpMarketplace"
@@ -61,6 +62,7 @@ export interface WebviewMessage {
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
| "fetchUserCreditsData"
| "optionsResponse"
| "requestTotalTasksSize"
// | "relaunchChromeDebugMode"
+23 -4
View File
@@ -21,6 +21,7 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
@@ -49,7 +50,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
"vite": "^6.2.1",
"vitest": "^3.0.5"
}
},
@@ -4177,6 +4178,12 @@
"layout-base": "^1.0.0"
}
},
"node_modules/countup.js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz",
"integrity": "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7095,6 +7102,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-countup": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz",
"integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==",
"license": "MIT",
"dependencies": {
"countup.js": "^2.8.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -8359,9 +8378,9 @@
}
},
"node_modules/vite": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -1
View File
@@ -25,6 +25,7 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
@@ -53,7 +54,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
"vite": "^6.2.1",
"vitest": "^3.0.5"
}
}
+2 -1
View File
@@ -42,7 +42,7 @@ const AppContent = () => {
setShowMcp(true)
setShowAccount(false)
break
case "accountLoginClicked":
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
@@ -96,6 +96,7 @@ const AppContent = () => {
showHistoryView={() => {
setShowSettings(false)
setShowMcp(false)
setShowAccount(false)
setShowHistory(true)
}}
isHidden={showSettings || showHistory || showMcp || showAccount}
+11
View File
@@ -0,0 +1,11 @@
import { SVGProps } from "react"
const ClineLogoWhite = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="white"
/>
</svg>
)
export default ClineLogoWhite
+114 -125
View File
@@ -1,8 +1,12 @@
import { VSCodeButton, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { VSCodeButton, VSCodeDivider, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
import { vscode } from "../../utils/vscode"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
type AccountViewProps = {
onDone: () => void
@@ -10,38 +14,13 @@ type AccountViewProps = {
const AccountView = ({ onDone }: AccountViewProps) => {
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: "10px 0px 0px 20px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Cline Account</h3>
<div className="fixed inset-0 flex flex-col overflow-hidden pt-[10px] pl-[20px]">
<div className="flex justify-between items-center mb-[17px] pr-[17px]">
<h3 className="text-[var(--vscode-foreground)] m-0">Cline Account</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div
style={{
flexGrow: 1,
overflowY: "scroll",
paddingRight: 8,
display: "flex",
flexDirection: "column",
}}>
<div style={{ marginBottom: 5 }}>
<div className="flex-grow overflow-hidden pr-[8px] flex flex-col">
<div className="h-full mb-[5px]">
<ClineAccountView />
</div>
</div>
@@ -51,6 +30,37 @@ const AccountView = ({ onDone }: AccountViewProps) => {
export const ClineAccountView = () => {
const { user, handleSignOut } = useFirebaseAuth()
const [balance, setBalance] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [usageData, setUsageData] = useState<UsageTransaction[]>([])
const [paymentsData, setPaymentsData] = useState<PaymentTransaction[]>([])
// Listen for balance and transaction data updates from the extension
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "userCreditsBalance" && message.userCreditsBalance) {
setBalance(message.userCreditsBalance.currentBalance)
} else if (message.type === "userCreditsUsage" && message.userCreditsUsage) {
setUsageData(message.userCreditsUsage.usageTransactions)
} else if (message.type === "userCreditsPayments" && message.userCreditsPayments) {
setPaymentsData(message.userCreditsPayments.paymentTransactions)
}
setIsLoading(false)
}
window.addEventListener("message", handleMessage)
// Fetch all account data when component mounts
if (user) {
setIsLoading(true)
vscode.postMessage({ type: "fetchUserCreditsData" })
}
return () => {
window.removeEventListener("message", handleMessage)
}
}, [user])
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
@@ -63,108 +73,87 @@ export const ClineAccountView = () => {
handleSignOut()
}
return (
<div style={{ maxWidth: "600px" }}>
<div className="h-full flex flex-col">
{user ? (
<div
style={{
padding: "8px 10px",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
backgroundColor: "var(--vscode-dropdown-background)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
{user.photoURL ? (
<img
src={user.photoURL}
alt="Profile"
style={{
width: 38,
height: 38,
borderRadius: "50%",
}}
/>
) : (
<div
style={{
width: 38,
height: 38,
borderRadius: "50%",
backgroundColor: "var(--vscode-button-background)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
color: "var(--vscode-button-foreground)",
}}>
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}>
{user.displayName && (
<div
style={{
fontSize: "13px",
fontWeight: "bold",
color: "var(--vscode-foreground)",
}}>
{user.displayName}
<div className="flex flex-col p-4 h-full">
<div className="flex flex-col w-full">
<div className="flex items-center mb-6 flex-wrap gap-y-4">
{user.photoURL ? (
<img src={user.photoURL} alt="Profile" className="size-16 rounded-full mr-4" />
) : (
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
{user.email && (
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
}}>
{user.email}
</div>
)}
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
<VSCodeButtonLink
href="https://app.cline.bot/credits"
appearance="primary"
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: 0,
marginRight: -12,
}}>
Account
</VSCodeButtonLink>
<VSCodeButton
appearance="secondary"
onClick={handleLogout}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: 0,
marginRight: -12,
}}>
Log out
</VSCodeButton>
<div className="flex flex-col">
{user.displayName && (
<h2 className="text-[var(--vscode-foreground)] m-0 mb-1 text-2xl font-medium">
{user.displayName}
</h2>
)}
{user.email && (
<div className="text-base text-[var(--vscode-descriptionForeground)]">{user.email}</div>
)}
</div>
</div>
</div>
<div className="w-full flex gap-2 flex-col min-[225px]:flex-row">
<div className="w-full min-[225px]:w-1/2">
<VSCodeButtonLink href="https://app.cline.bot/credits" appearance="primary" className="w-full">
Account
</VSCodeButtonLink>
</div>
<VSCodeButton appearance="secondary" onClick={handleLogout} className="w-full min-[225px]:w-1/2">
Log out
</VSCodeButton>
</div>
<VSCodeDivider className="w-full my-6" />
<div className="w-full flex flex-col items-center">
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6">
{isLoading ? (
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
) : (
<>
<span>$</span>
<CountUp end={balance} duration={0.66} decimals={2} />
</>
)}
</div>
<div className="w-full">
<VSCodeButtonLink href="https://app.cline.bot/credits/#buy" className="w-full">
Add Credits
</VSCodeButtonLink>
</div>
</div>
<VSCodeDivider className="mt-6 mb-10 w-full" />
<div className="flex-grow flex flex-col min-h-0 pb-[20px]">
<CreditsHistoryTable isLoading={isLoading} usageData={usageData} paymentsData={paymentsData} />
</div>
</div>
) : (
<div style={{}}>
<VSCodeButton onClick={handleLogin} style={{ marginTop: 0 }}>
Sign Up with Cline
<div className="flex flex-col items-center p-5 max-w-[400px]">
<ClineLogoWhite className="size-16 mb-4" />
<h2 className="text-[var(--vscode-foreground)] m-0 mb-6 text-2xl font-normal">Sign up with Cline</h2>
<VSCodeButton onClick={handleLogin} className="w-full mb-4">
Login with Cline
</VSCodeButton>
<p className="text-[var(--vscode-descriptionForeground)] text-xs text-center m-0">
By continuing, you agree to the <VSCodeLink href="https://cline.bot/tos">Terms of Service</VSCodeLink> and{" "}
<VSCodeLink href="https://cline.bot/privacy">Privacy Policy</VSCodeLink>.
</p>
</div>
)}
</div>
@@ -0,0 +1,114 @@
import { VSCodeDataGrid, VSCodeDataGridRow, VSCodeDataGridCell } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { TabButton } from "../mcp/McpView"
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
import { formatDollars, formatTimestamp } from "../../utils/format"
interface CreditsHistoryTableProps {
isLoading: boolean
usageData: UsageTransaction[]
paymentsData: PaymentTransaction[]
}
const CreditsHistoryTable = ({ isLoading, usageData, paymentsData }: CreditsHistoryTableProps) => {
const [activeTab, setActiveTab] = useState<"usage" | "payments">("usage")
return (
<div className="flex flex-col flex-grow h-full">
{/* Tabs container */}
<div className="flex border-b border-[var(--vscode-panel-border)]">
<TabButton isActive={activeTab === "usage"} onClick={() => setActiveTab("usage")}>
USAGE HISTORY
</TabButton>
<TabButton isActive={activeTab === "payments"} onClick={() => setActiveTab("payments")}>
PAYMENTS HISTORY
</TabButton>
</div>
{/* Content container */}
<div className="mt-[30px] mb-[20px] rounded-md overflow-auto flex-grow">
{isLoading ? (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
</div>
) : (
<>
{activeTab === "usage" && (
<>
{usageData.length > 0 ? (
<VSCodeDataGrid>
<VSCodeDataGridRow row-type="header">
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
Date
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
Model
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Tokens Used
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="4">
Credits Used
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{usageData.map((row, index) => (
<VSCodeDataGridRow key={index}>
<VSCodeDataGridCell grid-column="1">
{formatTimestamp(row.spentAt)}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">{`${row.modelProvider}/${row.model}`}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="3">{`${row.promptTokens}${row.completionTokens}`}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="4">{`$${Number(row.credits).toFixed(7)}`}</VSCodeDataGridCell>
</VSCodeDataGridRow>
))}
</VSCodeDataGrid>
) : (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">No usage history</div>
</div>
)}
</>
)}
{activeTab === "payments" && (
<>
{paymentsData.length > 0 ? (
<VSCodeDataGrid>
<VSCodeDataGridRow row-type="header">
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
Date
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
Total Cost
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Credits
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{paymentsData.map((row, index) => (
<VSCodeDataGridRow key={index}>
<VSCodeDataGridCell grid-column="1">
{formatTimestamp(row.paidAt)}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">{`$${formatDollars(parseInt(row.amountCents))}`}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="3">{`${row.credits}`}</VSCodeDataGridCell>
</VSCodeDataGridRow>
))}
</VSCodeDataGrid>
) : (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">No payment history</div>
</div>
)}
</>
)}
</>
)}
</div>
</div>
)
}
export default CreditsHistoryTable
@@ -30,7 +30,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ currentBalance, tot
</div>
<VSCodeButtonLink
href="https://app.cline.bot/credits"
href="https://app.cline.bot/credits/#buy"
style={{
width: "100%",
marginBottom: "8px",
@@ -52,7 +52,7 @@ import { vscode } from "../../utils/vscode"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
import AccountView, { ClineAccountView } from "../account/AccountView"
import { ClineAccountInfoCard } from "./ClineAccountInfoCard"
interface ApiOptionsProps {
showModelOptions: boolean
@@ -216,7 +216,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "cline" && (
<div style={{ marginBottom: 8, marginTop: 4 }}>
<ClineAccountView />
<ClineAccountInfoCard />
</div>
)}
@@ -0,0 +1,72 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
import { vscode } from "../../utils/vscode"
export const ClineAccountInfoCard = () => {
const { user, handleSignOut } = useFirebaseAuth()
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleLogout = () => {
// First notify extension to clear API keys and state
vscode.postMessage({ type: "accountLogoutClicked" })
// Then sign out of Firebase
handleSignOut()
}
const handleShowAccount = () => {
vscode.postMessage({ type: "showAccountViewClicked" })
}
return (
<div className="max-w-[600px]">
{user ? (
<div className="p-3 py-4 rounded-[2px] bg-[var(--vscode-dropdown-background)]">
<div className="flex items-center gap-3">
{user.photoURL ? (
<img src={user.photoURL} alt="Profile" className="w-[38px] h-[38px] rounded-full flex-shrink-0" />
) : (
<div className="w-[38px] h-[38px] rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-xl text-[var(--vscode-button-foreground)] flex-shrink-0">
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
<div className="flex flex-col gap-1 flex-1 overflow-hidden">
{user.displayName && (
<div className="text-[13px] font-bold text-[var(--vscode-foreground)] break-words">
{user.displayName}
</div>
)}
{user.email && (
<div className="text-[13px] text-[var(--vscode-descriptionForeground)] break-words overflow-hidden text-ellipsis">
{user.email}
</div>
)}
<div className="flex gap-2 flex-wrap mt-1">
<VSCodeButton
appearance="primary"
onClick={handleShowAccount}
className="scale-[0.85] origin-left w-fit mt-0.5 mb-0 -mr-3">
Account
</VSCodeButton>
<VSCodeButton
appearance="secondary"
onClick={handleLogout}
className="scale-[0.85] origin-left w-fit mt-0.5 mb-0 -mr-3">
Log out
</VSCodeButton>
</div>
</div>
</div>
</div>
) : (
<div>
<VSCodeButton onClick={handleLogin} className="mt-0">
Sign Up with Cline
</VSCodeButton>
</div>
)}
</div>
)
}
+24
View File
@@ -10,3 +10,27 @@ export function formatLargeNumber(num: number): string {
}
return num.toString()
}
// Helper to format cents as dollars with 2 decimal places
export function formatDollars(cents?: number): string {
if (cents === undefined) {
return ""
}
return (cents / 100).toFixed(2)
}
export function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp)
const dateFormatter = new Intl.DateTimeFormat("en-US", {
month: "2-digit",
day: "2-digit",
year: "2-digit",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
return dateFormatter.format(date)
}