Compare commits

...
Author SHA1 Message Date
Evan 1c0e295d74 format 2025-01-29 13:49:16 -08:00
pashpashpash 199bdedbed adjusting welcome page language 2025-01-28 12:47:34 -08:00
pashpashpash 1b59a33880 added cline provider + making sure welcome page closes 2025-01-28 11:45:03 -08:00
pashpashpash 6e65b869ae adding cline apikey to context 2025-01-28 10:58:01 -08:00
pashpashpash cf61a8961a welcome page now shows login button 2025-01-27 11:35:15 -08:00
13 changed files with 330 additions and 12 deletions
+3
View File
@@ -13,6 +13,7 @@ import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ClineHandler } from "./providers/cline"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -50,6 +51,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new MistralHandler(options)
case "vscode-lm":
return new VsCodeLmHandler(options)
case "cline":
return new ClineHandler(options)
default:
return new AnthropicHandler(options)
}
+148
View File
@@ -0,0 +1,148 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.clineApiKey || "",
baseURL: "https://api.cline.bot/v1",
defaultHeaders: {
"X-Firebase-Token": this.options.authToken || "",
},
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
try {
// TEST: Simulate credit limit error for testing UI
// const error = new Error("Credit limit reached") as any
// error.status = 402
// error.error = {
// type: "credit_limit_reached",
// message: "You have reached your credit limit. Please visit the billing page to add more credits.",
// credits_remaining: 0,
// credits_used: 1000,
// credits_limit: 1000,
// recharge_url: "https://cline.bot/billing",
// }
// throw error
const stream = await this.client.messages.create({
model: model.id,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [{ text: systemPrompt, type: "text" }],
messages,
stream: true,
})
for await (const chunk of stream) {
switch (chunk.type) {
case "message_start":
const usage = chunk.message.usage as any
const result: any = {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
}
if (usage.cache_creation_input_tokens) {
result.cacheWriteTokens = usage.cache_creation_input_tokens
}
if (usage.cache_read_input_tokens) {
result.cacheReadTokens = usage.cache_read_input_tokens
}
yield result
break
case "message_delta":
yield {
type: "usage",
inputTokens: 0,
outputTokens: chunk.usage.output_tokens || 0,
}
break
case "content_block_start":
switch (chunk.content_block.type) {
case "text":
if (chunk.index > 0) {
yield {
type: "text",
text: "\n",
}
}
yield {
type: "text",
text: chunk.content_block.text,
}
break
}
break
case "content_block_delta":
switch (chunk.delta.type) {
case "text_delta":
yield {
type: "text",
text: chunk.delta.text,
}
break
}
break
}
}
} catch (error: any) {
// Handle credit limit errors
if (error.status === 402 && error.error?.type === "credit_limit_reached") {
// Yield a credit_limit_reached message that will be transformed into a ClineMessage
// with say="credit_limit_reached" by the Cline class
yield {
type: "text",
text: JSON.stringify({
type: "credit_limit_reached",
creditsRemaining: error.error.credits_remaining,
creditsUsed: error.error.credits_used,
creditsLimit: error.error.credits_limit,
rechargeUrl: error.error.recharge_url,
message: error.error.message,
}),
}
return
}
// For other errors, yield an error message that will be transformed
// into a ClineMessage with say="error"
const errorMessage = error.error?.message || error.message || "An unknown error occurred"
yield {
type: "text",
text: JSON.stringify({
type: "error",
message: errorMessage,
}),
}
}
}
getModel() {
return {
id: "claude-3-5-sonnet",
info: {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
},
}
}
}
+31 -1
View File
@@ -37,6 +37,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
type SecretKey =
| "apiKey"
| "clineApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
@@ -138,6 +139,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async handleSignOut() {
try {
await this.authManager.signOut()
await this.storeSecret("authToken", undefined)
await this.storeSecret("clineApiKey", undefined)
await this.updateGlobalState("apiProvider", "openrouter")
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged out of Cline")
} catch (error) {
vscode.window.showErrorMessage("Logout failed")
@@ -836,13 +841,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return true
}
async handleAuthCallback(token: string) {
async handleAuthCallback(token: string, apiKey: string) {
try {
// First sign in with Firebase to trigger auth state change
await this.authManager.signInWithCustomToken(token)
// Then store the token securely
await this.storeSecret("authToken", token)
await this.storeSecret("clineApiKey", apiKey)
const clineProvider: ApiProvider = "cline"
await this.updateGlobalState("apiProvider", clineProvider)
// Update API configuration with the new provider and auth token
const { apiConfiguration } = await this.getState()
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
clineApiKey: apiKey,
authToken: token,
}
if (this.cline) {
this.cline.api = buildApiHandler(updatedConfig)
}
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged in to Cline")
} catch (error) {
@@ -1230,6 +1253,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -1261,11 +1285,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
vsCodeLmModelSelector,
localeLanguage,
userInfo,
authToken,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
this.getSecret("apiKey") as Promise<string | undefined>,
this.getSecret("openRouterApiKey") as Promise<string | undefined>,
this.getSecret("clineApiKey") as Promise<string | undefined>,
this.getSecret("awsAccessKey") as Promise<string | undefined>,
this.getSecret("awsSecretKey") as Promise<string | undefined>,
this.getSecret("awsSessionToken") as Promise<string | undefined>,
@@ -1297,6 +1323,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
this.getGlobalState("localeLanguage") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@@ -1319,6 +1346,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -1342,6 +1370,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
authToken,
},
lastShownAnnouncementId,
customInstructions,
@@ -1428,6 +1457,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"deepSeekApiKey",
"mistralApiKey",
"authToken",
"clineApiKey",
]
for (const key of secretKeys) {
await this.storeSecret(key, undefined)
+4 -2
View File
@@ -158,10 +158,12 @@ export function activate(context: vscode.ExtensionContext) {
case "/auth": {
const token = query.get("token")
const state = query.get("state")
const apiKey = query.get("apiKey")
console.log("Auth callback received:", {
token: token,
state: state,
apiKey: apiKey,
})
// Validate state parameter
@@ -170,8 +172,8 @@ export function activate(context: vscode.ExtensionContext) {
return
}
if (token) {
await visibleProvider.handleAuthCallback(token)
if (token && apiKey) {
await visibleProvider.handleAuthCallback(token, apiKey)
}
break
}
+1
View File
@@ -100,6 +100,7 @@ export type ClineAsk =
export type ClineSay =
| "task"
| "error"
| "credit_limit_reached"
| "api_req_started"
| "api_req_finished"
| "text"
+3
View File
@@ -11,10 +11,13 @@ export type ApiProvider =
| "deepseek"
| "mistral"
| "vscode-lm"
| "cline"
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
clineApiKey?: string
authToken?: string // firebase auth token for cline provider
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
@@ -1,6 +1,7 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import CreditLimitError from "./CreditLimitError"
import { useEvent, useSize } from "react-use"
import styled from "styled-components"
import {
@@ -875,6 +876,15 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
/>
</div>
)
case "credit_limit_reached":
const errorData = JSON.parse(message.text || "{}")
return (
<CreditLimitError
creditsRemaining={errorData.creditsRemaining}
creditsUsed={errorData.creditsUsed}
rechargeUrl={errorData.rechargeUrl}
/>
)
case "error":
return (
<>
@@ -0,0 +1,46 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import { vscode } from "../../utils/vscode"
interface CreditLimitErrorProps {
creditsRemaining: number
creditsUsed: number
rechargeUrl: string
}
const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ creditsRemaining, creditsUsed, rechargeUrl }) => {
return (
<div
role="alert"
style={{
backgroundColor: "var(--vscode-errorBackground)",
padding: "12px",
borderRadius: "3px",
marginBottom: "10px",
border: "1px solid var(--vscode-errorBorder)",
}}>
<div style={{ display: "flex", alignItems: "center", marginBottom: "8px" }}>
<i
className="codicon codicon-error"
style={{ color: "var(--vscode-errorForeground)", marginRight: "8px" }}
aria-hidden="true"
/>
<span style={{ fontWeight: "bold", color: "var(--vscode-errorForeground)" }}>Credit Limit Reached</span>
</div>
<div style={{ marginBottom: "12px", color: "var(--vscode-foreground)" }}>
<div style={{ marginBottom: "4px" }}>Credits Remaining: {creditsRemaining.toLocaleString()}</div>
<div>Credits Used: {creditsUsed.toLocaleString()}</div>
</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton appearance="primary" onClick={() => window.open(rechargeUrl, "_blank")}>
Add Credits
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={() => vscode.postMessage({ type: "clearTask" })}>
Start New Task
</VSCodeButton>
</div>
</div>
)
}
export default CreditLimitError
@@ -6,6 +6,7 @@ import {
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
VSCodeButton,
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
@@ -84,6 +85,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const handleClineLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
}
@@ -170,6 +175,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
value={selectedProvider}
onChange={handleInputChange("apiProvider")}
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
<VSCodeOption value="cline">Cline</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
@@ -185,6 +191,40 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</VSCodeDropdown>
</DropdownContainer>
{selectedProvider === "cline" && (
<div>
{apiConfiguration?.clineApiKey && (
<VSCodeTextField
value={apiConfiguration.clineApiKey}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("clineApiKey")}
placeholder={t("enterApiKey")}>
<span style={{ fontWeight: 500 }}>Cline API Key</span>
</VSCodeTextField>
)}
{apiConfiguration?.clineApiKey && (
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{t("apiKeyInfo")}
</p>
)}
{!apiConfiguration?.clineApiKey && (
<div style={{ marginTop: 2 }}>
<VSCodeButton appearance="primary" onClick={handleClineLogin}>
Get Cline API Key
</VSCodeButton>
</div>
)}
</div>
)}
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
@@ -963,6 +1003,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
switch (provider) {
case "cline":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "bedrock":
@@ -8,7 +8,7 @@ import ApiOptions from "./ApiOptions"
import LanguageOptions from "./LanguageOptions"
import SettingsButton from "../common/SettingsButton"
const IS_DEV = false // FIXME: use flags when packaging
const IS_DEV = true // FIXME: use flags when packaging
type SettingsViewProps = {
onDone: () => void
@@ -1,4 +1,4 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeDivider, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration } from "../../utils/validate"
@@ -7,11 +7,15 @@ import ApiOptions from "../settings/ApiOptions"
const WelcomeView = () => {
const { apiConfiguration } = useExtensionState()
const [showApiOptions, setShowApiOptions] = useState(false)
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiErrorMessage != null
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleSubmit = () => {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
}
@@ -29,6 +33,8 @@ const WelcomeView = () => {
right: 0,
bottom: 0,
padding: "0 20px",
display: "flex",
flexDirection: "column",
}}>
<h2>Hi, I'm Cline</h2>
<p>
@@ -43,13 +49,34 @@ const WelcomeView = () => {
capabilities.
</p>
<b>To get started, this extension needs an API provider for Claude 3.5 Sonnet.</b>
<div style={{ marginTop: "10px" }}>
<ApiOptions showModelOptions={false} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
<div style={{ marginTop: "20px", marginBottom: "20px" }}>
<VSCodeButton appearance="primary" onClick={handleLogin}>
Log in to Cline
</VSCodeButton>
<div style={{ marginTop: "10px" }}>
<ul style={{ paddingLeft: "20px", margin: "10px 0" }}>
<li>Get 1 task worth of free tokens</li>
<li>No credit card required - just start using Cline immediately!</li>
</ul>
</div>
</div>
<VSCodeDivider />
<div style={{ marginTop: "20px" }}>
<VSCodeButton appearance="secondary" onClick={() => setShowApiOptions(!showApiOptions)}>
{showApiOptions ? "Hide API options" : "Use your own provider API key"}
</VSCodeButton>
{showApiOptions && (
<div style={{ marginTop: "10px" }}>
<ApiOptions showModelOptions={false} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
</div>
)}
</div>
</div>
)
@@ -70,6 +70,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.deepSeekApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
+5
View File
@@ -43,6 +43,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "cline":
if (!apiConfiguration.clineApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
case "openai":
if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
return "You must provide a valid base URL, API key, and model ID."