Compare commits

...
Author SHA1 Message Date
Saoud Rizwan 4a9504e965 Fix errors 2025-03-05 14:47:54 -08:00
Saoud Rizwan de3581f2d2 Merge branch 'main' into pashpashpash/accounts 2025-03-05 14:33:40 -08:00
Saoud Rizwan 563810fc60 Fixes 2025-03-05 00:17:42 -08:00
Saoud Rizwan 76e1b68115 Update welcome page and cline account view 2025-03-05 00:13:22 -08:00
Saoud Rizwan 17da90ff47 Remove email subscribe 2025-03-04 22:11:34 -08:00
Saoud Rizwan 477e2a2695 Remove duplicate csp 2025-03-04 21:53:50 -08:00
Saoud Rizwan 3ecabb559e Add missing options to cline stream 2025-03-04 21:40:42 -08:00
Saoud Rizwan 8f8de75dc8 Fix package-lock 2025-03-04 21:24:33 -08:00
pashpashpash 78603bfd6b removing redundant csp allowance 2025-03-04 15:13:40 -08:00
pashpashpash 89ad9a241c resolving merge conflicts 2025-03-04 15:12:46 -08:00
pashpashpash 53582d6149 package-lock.json straight from main 2025-03-02 23:23:30 -08:00
pashpashpash ac53b9e66b reverting package-lock changes 2025-03-02 23:19:31 -08:00
pashpashpash b8c5aeaff5 deleting packagejson line for vertexai 2025-03-02 23:18:45 -08:00
pashpashpash 7ae6c0cfa3 use standard API Request Failed title for all API errors while maintaining detailed credit limit information in error content 2025-03-02 23:18:09 -08:00
pashpashpash 28fb2ae69c packagejson + lock 2025-03-01 18:32:05 -08:00
pashpashpash f05d3e9295 package-lock 2025-03-01 18:19:47 -08:00
pashpashpash 29c16ea7c8 reconciling merge conflicts 2025-03-01 18:15:41 -08:00
pashpashpash 20bab1edfa package lock 2025-02-20 15:29:34 -08:00
pashpashpash 8755bf9805 package-lock.json 2025-02-20 15:20:01 -08:00
pashpashpash 24a38e1471 reconciling merge conflicts 2025-02-20 15:18:57 -08:00
pashpashpash 4b12f2aaca reconciling merge conflicts 2025-02-20 15:17:33 -08:00
pashpashpash c7ac62ad21 reconciling merge conflicts 2025-02-19 20:20:41 -08:00
pashpashpash 32dcdcf038 reconciling merge conflicts with main 2025-02-19 20:12:13 -08:00
pashpashpash f7ee18cd88 resolving merge conflicts (againnnnn) 2025-02-09 23:52:55 -08:00
pashpashpash 0f7b726b70 cleaned up all old logic for extension server side auth persistence 2025-02-09 02:31:41 -08:00
pashpashpash 2fde7642c2 moving firebase auth logic to webview because nodejs firebase token refresh is not supported 2025-02-09 02:18:23 -08:00
pashpashpash 82d9385b8b even more aggressive custom token fetching + storing in apiConfig 2025-02-08 20:37:29 -08:00
pashpashpash 5a3a9f1392 resolving all merge conflicts 2025-02-08 17:49:27 -08:00
pashpashpash 958b28658b only removing custom token on explicit log out action 2025-02-08 17:16:57 -08:00
pashpashpash 8ab768e770 more logging and updated authentication system to be more robust and reliable 2025-02-08 14:33:17 -08:00
pashpashpash 03af507fe6 welcome page language 2025-02-06 17:18:09 -08:00
pashpashpash b1cd61d909 Update tricky-zebras-talk.md 2025-02-05 16:42:33 -08:00
pashpashpash b7d2ce9429 Update tricky-zebras-talk.md 2025-02-05 16:42:20 -08:00
pashpashpash 11bc4e6f94 changeset added 2025-02-05 16:41:19 -08:00
pashpashpash b504224ab7 set isdev to false for prerelease 2025-02-05 03:02:27 -08:00
pashpashpash 787863f7b4 merge conflicts resolved 2025-02-05 02:51:17 -08:00
pashpashpash 289ca46515 response.data instead of response.data.data 2025-02-05 02:38:00 -08:00
pashpashpash 914e574be3 logging cline instead of openrouter 2025-02-05 02:22:06 -08:00
pashpashpash 5625a93fcc logging cline instead of openrouter 2025-02-05 02:21:28 -08:00
pashpashpash 5b3b710003 small edit to account view 2025-02-05 02:18:35 -08:00
pashpashpash 1e735da806 added generation details tracking to cline provider 2025-02-05 02:05:06 -08:00
Saoud Rizwan 0c8df6aa61 Fix error handling 2025-02-04 14:36:16 -08:00
Saoud Rizwan c06e91d459 Update welcome experience 2025-02-04 14:17:20 -08:00
Saoud Rizwan 57e64ba50c use openrouter format with cline api 2025-02-04 14:03:10 -08:00
pashpashpash d4da786018 renaming Account -> Cline Account 2025-02-04 12:47:09 -08:00
pashpashpash dcc06b2e3d persisting login state through window reloads 2025-02-04 12:45:29 -08:00
pashpashpash 56de6dfc4e added back account page and relying more heavily on the firebase custom token to ensure persistent login 2025-02-04 12:17:54 -08:00
pashpashpash db294ff45a welcome view hiding api options by default 2025-02-03 16:50:16 -08:00
pashpashpash 5c59f19fbf Merge remote-tracking branch 'origin/main' into pashpashpash/accounts 2025-02-03 16:42:29 -08:00
pashpashpash 3c69365f7a handling credit limit errors (super hacky ugh) 2025-02-03 16:28:12 -08:00
pashpashpash 82f0620ab7 swapped cline provider to use openai flavor requests 2025-01-29 15:55:08 -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
26 changed files with 1787 additions and 546 deletions
+11
View File
@@ -0,0 +1,11 @@
---
"claude-dev": minor
---
Added Cline as a new provider with native authentication, account management, and credit limit handling. Updates include:
- Native authentication flow with email and Google OAuth
- Account management page showing user information
- Credit limit error handling and UI
- Updated welcome experience with Cline login option
- Integration using OpenRouter request format
- Firebase customer state persistence"
+3
View File
@@ -16,6 +16,7 @@ import { TogetherHandler } from "./providers/together"
import { QwenHandler } from "./providers/qwen"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
@@ -62,6 +63,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new MistralHandler(options)
case "vscode-lm":
return new VsCodeLmHandler(options)
case "cline":
return new ClineHandler(options)
case "litellm":
return new LiteLlmHandler(options)
case "asksage":
+62
View File
@@ -0,0 +1,62 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import axios from "axios"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
})
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const genId = yield* streamOpenRouterFormatRequest(
this.client,
systemPrompt,
messages,
model,
this.options.o3MiniReasoningEffort,
this.options.thinkingBudgetTokens,
)
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${genId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
},
timeout: 5_000, // this request hangs sometimes
})
const generation = response.data
console.log("cline generation details:", generation)
yield {
type: "usage",
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} catch (error) {
// ignore if fails
console.error("Error fetching cline generation details:", error)
}
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.openRouterModelId
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
}
}
+9 -192
View File
@@ -5,7 +5,7 @@ import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { OpenRouterErrorResponse } from "./types"
@@ -29,197 +29,14 @@ export class OpenRouterHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
// Convert Anthropic messages to OpenAI format
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
break
default:
break
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
maxTokens = 8_192
break
}
let temperature: number | undefined = 0
let topP: number | undefined = undefined
if (this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning") {
// Recommended values from DeepSeek
temperature = 0.7
topP = 0.95
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
let reasoning: { max_tokens: number } | undefined = undefined
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0 ? true : false
if (reasoningOn) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
}
break
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
if (model.id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
model: model.id,
max_tokens: maxTokens,
temperature: temperature,
top_p: topP,
messages: openAiMessages,
stream: true,
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
})
let genId: string | undefined
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!genId && chunk.id) {
genId = chunk.id
}
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// console.log("reasoning", delta.reasoning)
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
// if (didStreamThinkTagInReasoning) {
// yield {
// type: "text",
// // @ts-ignore-next-line
// text: delta.reasoning,
// }
// } else {
// yield {
// type: "reasoning",
// // @ts-ignore-next-line
// text: delta.reasoning,
// }
// // @ts-ignore-next-line
// reasoningResponse += delta.reasoning
// if (reasoningResponse.includes("</think>")) {
// didStreamThinkTagInReasoning = true
// console.log("did hit think tag", reasoningResponse)
// }
// }
}
// if (chunk.usage) {
// yield {
// type: "usage",
// inputTokens: chunk.usage.prompt_tokens || 0,
// outputTokens: chunk.usage.completion_tokens || 0,
// }
// }
}
const genId = yield* streamOpenRouterFormatRequest(
this.client,
systemPrompt,
messages,
model,
this.options.o3MiniReasoningEffort,
this.options.thinkingBudgetTokens,
)
if (genId) {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
+180
View File
@@ -0,0 +1,180 @@
import { ModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "./openai-format"
import { convertToR1Format } from "./r1-format"
import { ApiStream, ApiStreamChunk } from "./stream"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { OpenRouterErrorResponse } from "../providers/types"
export async function* streamOpenRouterFormatRequest(
client: OpenAI,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
o3MiniReasoningEffort?: string,
thinkingBudgetTokens?: number,
): AsyncGenerator<ApiStreamChunk, string | undefined, unknown> {
// Convert Anthropic messages to OpenAI format
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
break
default:
break
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
maxTokens = 8_192
break
}
let temperature: number | undefined = 0
let topP: number | undefined = undefined
if (model.id.startsWith("deepseek/deepseek-r1") || model.id === "perplexity/sonar-reasoning") {
// Recommended values from DeepSeek
temperature = 0.7
topP = 0.95
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
let reasoning: { max_tokens: number } | undefined = undefined
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
let budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0 ? true : false
if (reasoningOn) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
}
break
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
if (model.id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
}
// @ts-ignore-next-line
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: maxTokens,
temperature: temperature,
top_p: topP,
messages: openAiMessages,
stream: true,
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id === "openai/o3-mini" ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
})
let genId: string | undefined
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!genId && chunk.id) {
genId = chunk.id
}
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
}
return genId
}
+4 -3
View File
@@ -9,9 +9,7 @@ import * as path from "path"
import { serializeError } from "serialize-error"
import * as vscode from "vscode"
import { ApiHandler, buildApiHandler } from "../api"
import { OpenAiHandler } from "../api/providers/openai"
import { OpenRouterHandler } from "../api/providers/openrouter"
import { ApiStream } from "../api/transform/stream"
import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
@@ -58,6 +56,9 @@ import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
import { OpenAiHandler } from "../api/providers/openai"
import { ApiStream } from "../api/transform/stream"
import { ClineHandler } from "../api/providers/cline"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
import { telemetryService } from "../services/telemetry/TelemetryService"
@@ -1364,7 +1365,7 @@ export class Cline {
yield firstChunk.value
this.isWaitingForFirstChunk = false
} catch (error) {
const isOpenRouter = this.api instanceof OpenRouterHandler
const isOpenRouter = this.api instanceof OpenRouterHandler || this.api instanceof ClineHandler
if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
console.log("first chunk failed, waiting 1 second before retrying")
await delay(1000)
+47 -59
View File
@@ -14,8 +14,8 @@ 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 { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager"
import { McpHub } from "../../services/mcp/McpHub"
import { UserInfo } from "../../shared/UserInfo"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
@@ -34,7 +34,6 @@ import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { validateThinkingBudget } from "../../utils/validation"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
@@ -46,6 +45,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
type SecretKey =
| "apiKey"
| "clineApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
@@ -59,7 +59,6 @@ type SecretKey =
| "qwenApiKey"
| "mistralApiKey"
| "liteLlmApiKey"
| "authToken"
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
@@ -124,7 +123,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
private authManager: FirebaseAuthManager
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
constructor(
@@ -135,7 +133,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
ClineProvider.activeInstances.add(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.mcpHub = new McpHub(this)
this.authManager = new FirebaseAuthManager(this)
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -166,7 +163,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.workspaceTracker = undefined
this.mcpHub?.dispose()
this.mcpHub = undefined
this.authManager.dispose()
this.outputChannel.appendLine("Disposed all disposables")
ClineProvider.activeInstances.delete(this)
}
@@ -174,17 +170,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Auth methods
async handleSignOut() {
try {
await this.authManager.signOut()
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")
}
}
async setAuthToken(token?: string) {
await this.storeSecret("authToken", token)
}
async setUserInfo(info?: { displayName: string | null; email: string | null; photoURL: string | null }) {
await this.updateGlobalState("userInfo", info)
}
@@ -384,7 +378,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' https://*.posthog.com;">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com; https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
<body>
@@ -480,6 +474,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
webview.onDidReceiveMessage(
async (message: WebviewMessage) => {
switch (message.type) {
case "authStateChanged":
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
@@ -592,6 +590,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@@ -638,6 +637,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("asksageApiKey", asksageApiKey)
await this.updateGlobalState("asksageApiUrl", asksageApiUrl)
await this.updateGlobalState("thinkingBudgetTokens", thinkingBudgetTokens)
await this.storeSecret("clineApiKey", clineApiKey)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@@ -794,9 +794,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "getLatestState":
await this.postStateToWebview()
break
case "subscribeEmail":
this.subscribeEmail(message.text)
break
case "accountLoginClicked": {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
@@ -1011,6 +1008,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
case "cline":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
@@ -1048,6 +1046,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
case "cline":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
@@ -1080,12 +1079,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("chatSettings", chatSettings)
await this.postStateToWebview()
// console.log("chatSettings", message.chatSettings)
if (this.cline) {
this.cline.updateChatSettings(chatSettings)
if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
this.cline.didRespondToPlanAskBySwitchingMode = true
// this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
// Use chatContent if provided, otherwise use default message
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
@@ -1098,36 +1097,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
async subscribeEmail(email?: string) {
if (!email) {
return
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
vscode.window.showErrorMessage("Please enter a valid email address")
return
}
console.log("Subscribing email:", email)
this.postMessageToWebview({ type: "emailSubscribed" })
// Currently ignoring errors to this endpoint, but after accounts we'll remove this anyways
try {
const response = await axios.post(
"https://app.cline.bot/api/mailing-list",
{
email: email,
},
{
headers: {
"Content-Type": "application/json",
},
},
)
console.log("Email subscribed successfully. Response:", response.data)
} catch (error) {
console.error("Failed to subscribe email:", error)
}
}
async cancelTask() {
if (this.cline) {
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
@@ -1282,18 +1251,39 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return true
}
async handleAuthCallback(token: string) {
async handleAuthCallback(customToken: string, apiKey: string) {
try {
// First sign in with Firebase to trigger auth state change
await this.authManager.signInWithCustomToken(token)
// Store API key for API calls
await this.storeSecret("clineApiKey", apiKey)
// Send custom token to webview for Firebase auth
await this.postMessageToWebview({
type: "authCallback",
customToken,
})
const clineProvider: ApiProvider = "cline"
await this.updateGlobalState("apiProvider", clineProvider)
// Update API configuration with the new provider and API key
const { apiConfiguration } = await this.getState()
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
clineApiKey: apiKey,
}
if (this.cline) {
this.cline.api = buildApiHandler(updatedConfig)
}
// Then store the token securely
await this.storeSecret("authToken", token)
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged in to Cline")
} catch (error) {
console.error("Failed to handle auth callback:", error)
vscode.window.showErrorMessage("Failed to log in to Cline")
// Even on login failure, we preserve any existing tokens
// Only clear tokens on explicit logout
}
}
@@ -1775,7 +1765,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
browserSettings,
chatSettings,
userInfo,
authToken,
mcpMarketplaceEnabled,
telemetrySetting,
} = await this.getState()
@@ -1794,7 +1783,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
autoApprovalSettings,
browserSettings,
chatSettings,
isLoggedIn: !!authToken,
userInfo,
mcpMarketplaceEnabled,
telemetrySetting,
@@ -1859,6 +1847,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -1901,7 +1890,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
@@ -1918,6 +1906,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
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>,
@@ -1960,7 +1949,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("liteLlmBaseUrl") as Promise<string | undefined>,
this.getGlobalState("liteLlmModelId") as Promise<string | undefined>,
this.getGlobalState("userInfo") as Promise<UserInfo | undefined>,
this.getSecret("authToken") as Promise<string | undefined>,
this.getGlobalState("previousModeApiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("previousModeModelId") as Promise<string | undefined>,
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
@@ -1983,7 +1971,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to openrouter
// New users should default to openrouter, since they've opted to use an API key instead of signing in
apiProvider = "openrouter"
}
}
@@ -2000,6 +1988,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -2050,7 +2039,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
authToken,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
@@ -2184,8 +2172,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
"togetherApiKey",
"qwenApiKey",
"mistralApiKey",
"clineApiKey",
"liteLlmApiKey",
"authToken",
"asksageApiKey",
"xaiApiKey",
]
+4 -2
View File
@@ -162,10 +162,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
@@ -174,8 +176,8 @@ export function activate(context: vscode.ExtensionContext) {
return
}
if (token) {
await visibleProvider.handleAuthCallback(token)
if (token && apiKey) {
await visibleProvider.handleAuthCallback(token, apiKey)
}
break
}
-99
View File
@@ -1,99 +0,0 @@
import { initializeApp } from "firebase/app"
import { Auth, User, getAuth, onAuthStateChanged, signInWithCustomToken, signOut } from "firebase/auth"
import * as vscode from "vscode"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { firebaseConfig } from "./config"
export interface UserInfo {
displayName: string | null
email: string | null
photoURL: string | null
}
export class FirebaseAuthManager {
private providerRef: WeakRef<ClineProvider>
private auth: Auth
private disposables: vscode.Disposable[] = []
constructor(provider: ClineProvider) {
console.log("Initializing FirebaseAuthManager", { provider })
this.providerRef = new WeakRef(provider)
const app = initializeApp(firebaseConfig)
this.auth = getAuth(app)
console.log("Firebase app initialized", { appConfig: firebaseConfig })
// Auth state listener
onAuthStateChanged(this.auth, this.handleAuthStateChange.bind(this))
console.log("Auth state change listener added")
// Try to restore session
this.restoreSession()
}
private async restoreSession() {
console.log("Attempting to restore session")
const provider = this.providerRef.deref()
if (!provider) {
console.log("Provider reference lost during session restore")
return
}
const storedToken = await provider.getSecret("authToken")
if (storedToken) {
console.log("Found stored auth token, attempting to restore session")
try {
await this.signInWithCustomToken(storedToken)
console.log("Session restored successfully")
} catch (error) {
console.error("Failed to restore session, clearing token:", error)
await provider.setAuthToken(undefined)
await provider.setUserInfo(undefined)
}
} else {
console.log("No stored auth token found")
}
}
private async handleAuthStateChange(user: User | null) {
console.log("Auth state changed", { user })
const provider = this.providerRef.deref()
if (!provider) {
console.log("Provider reference lost")
return
}
if (user) {
console.log("User signed in", { userId: user.uid })
const idToken = await user.getIdToken()
await provider.setAuthToken(idToken)
// Store public user info in state
await provider.setUserInfo({
displayName: user.displayName,
email: user.email,
photoURL: user.photoURL,
})
console.log("User info set in provider", { user })
} else {
console.log("User signed out")
await provider.setAuthToken(undefined)
await provider.setUserInfo(undefined)
}
await provider.postStateToWebview()
console.log("Webview state updated")
}
async signInWithCustomToken(token: string) {
console.log("Signing in with custom token", { token })
await signInWithCustomToken(this.auth, token)
}
async signOut() {
console.log("Signing out")
await signOut(this.auth)
}
dispose() {
this.disposables.forEach((d) => d.dispose())
console.log("Disposables disposed", { count: this.disposables.length })
}
}
+2 -2
View File
@@ -27,7 +27,7 @@ export interface ExtensionMessage {
| "relinquishControl"
| "vsCodeLmModels"
| "requestVsCodeLmModels"
| "emailSubscribed"
| "authCallback"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
@@ -53,6 +53,7 @@ export interface ExtensionMessage {
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
mcpServers?: McpServer[]
customToken?: string
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpDownloadResponse
@@ -86,7 +87,6 @@ export interface ExtensionState {
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
chatSettings: ChatSettings
isLoggedIn: boolean
platform: Platform
userInfo?: {
displayName: string | null
+5
View File
@@ -0,0 +1,5 @@
export interface UserInfo {
displayName: string | null
email: string | null
photoURL: string | null
}
+6 -1
View File
@@ -2,6 +2,7 @@ import { ApiConfiguration } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { UserInfo } from "./UserInfo"
import { ChatContent } from "./ChatContent"
export interface WebviewMessage {
@@ -44,7 +45,8 @@ export interface WebviewMessage {
| "getLatestState"
| "accountLoginClicked"
| "accountLogoutClicked"
| "subscribeEmail"
| "authStateChanged"
| "authCallback"
| "fetchMcpMarketplace"
| "downloadMcp"
| "silentlyRefreshMcpMarketplace"
@@ -75,6 +77,9 @@ export interface WebviewMessage {
toolName?: string
autoApprove?: boolean
// For auth
user?: UserInfo | null
customToken?: string
// For openInBrowser
url?: string
}
+2
View File
@@ -14,6 +14,7 @@ export type ApiProvider =
| "qwen"
| "mistral"
| "vscode-lm"
| "cline"
| "litellm"
| "asksage"
| "xai"
@@ -21,6 +22,7 @@ export type ApiProvider =
export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
clineApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmApiKey?: string
+1002 -7
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -18,6 +18,7 @@
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"fuse.js": "^7.0.0",
"fzf": "^0.5.2",
"mermaid": "^11.4.1",
+4 -1
View File
@@ -7,6 +7,7 @@ import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/McpView"
@@ -112,7 +113,9 @@ const AppContent = () => {
const App = () => {
return (
<ExtensionStateContextProvider>
<AppContent />
<FirebaseAuthProvider>
<AppContent />
</FirebaseAuthProvider>
</ExtensionStateContextProvider>
)
}
+126 -36
View File
@@ -1,23 +1,14 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
import { vscode } from "../../utils/vscode"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
type AccountViewProps = {
onDone: () => void
}
const AccountView = ({ onDone }: AccountViewProps) => {
const { isLoggedIn, userInfo } = useExtensionState()
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleLogout = () => {
vscode.postMessage({ type: "accountLogoutClicked" })
}
return (
<div
style={{
@@ -39,7 +30,7 @@ const AccountView = ({ onDone }: AccountViewProps) => {
marginBottom: "17px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Account</h3>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Cline Account</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div
@@ -51,33 +42,132 @@ const AccountView = ({ onDone }: AccountViewProps) => {
flexDirection: "column",
}}>
<div style={{ marginBottom: 5 }}>
{isLoggedIn ? (
<>
{userInfo?.photoURL && (
<img
src={userInfo.photoURL}
alt="Profile"
style={{
width: 48,
height: 48,
borderRadius: "50%",
marginBottom: 10,
}}
/>
)}
<div style={{ fontSize: "14px", marginBottom: 10 }}>
{userInfo?.displayName && <div>Name: {userInfo.displayName}</div>}
{userInfo?.email && <div>Email: {userInfo.email}</div>}
</div>
<VSCodeButton onClick={handleLogout}>Log out</VSCodeButton>
</>
) : (
<VSCodeButton onClick={handleLogin}>Log in to Cline</VSCodeButton>
)}
<ClineAccountView />
</div>
</div>
</div>
)
}
export const ClineAccountView = () => {
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()
}
return (
<div style={{ maxWidth: "600px" }}>
{user ? (
<div
style={{
padding: "12px 16px",
border: "1px solid var(--vscode-widget-border)",
borderRadius: "6px",
backgroundColor: "var(--vscode-editor-background)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
}}>
{user.photoURL ? (
<img
src={user.photoURL}
alt="Profile"
style={{
width: 48,
height: 48,
borderRadius: "50%",
}}
/>
) : (
<div
style={{
width: 48,
height: 48,
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>
)}
{user.email && (
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
}}>
{user.email}
</div>
)}
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButtonLink
href="https://app.cline.bot/account"
appearance="primary"
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: -2,
marginRight: -8,
}}>
Account
</VSCodeButtonLink>
<VSCodeButton
appearance="secondary"
onClick={handleLogout}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: -2,
}}>
Log out
</VSCodeButton>
</div>
</div>
</div>
</div>
) : (
<div style={{}}>
<VSCodeButton onClick={handleLogin} style={{ marginTop: 0 }}>
Sign Up with Cline
</VSCodeButton>
</div>
)}
</div>
)
}
export default memo(AccountView)
+82 -77
View File
@@ -24,6 +24,7 @@ import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import { highlightMentions } from "./TaskHeader"
import CreditLimitError from "./CreditLimitError"
import { CheckmarkControl } from "../common/CheckmarkControl"
import McpResponseDisplay from "../mcp/McpResponseDisplay"
@@ -283,31 +284,25 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
) : (
<ProgressIndicator />
),
apiReqCancelReason != null ? (
apiReqCancelReason === "user_cancelled" ? (
<span
style={{
color: normalColor,
fontWeight: "bold",
}}>
API Request Cancelled
</span>
) : (
<span
style={{
color: errorColor,
fontWeight: "bold",
}}>
API Streaming Failed
</span>
)
) : cost != null ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
) : apiRequestFailedMessage ? (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
) : (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
),
(() => {
if (apiReqCancelReason != null) {
return apiReqCancelReason === "user_cancelled" ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
) : (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
)
}
if (cost != null) {
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
}
if (apiRequestFailedMessage) {
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
}
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
})(),
]
case "followup":
return [
@@ -729,62 +724,55 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
<>
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{(() => {
// Try to parse the error message as JSON for credit limit error
const errorData = parseErrorText(apiRequestFailedMessage)
if (errorData) {
if (
errorData.code === "insufficient_credits" &&
typeof errorData.current_balance === "number" &&
typeof errorData.total_spent === "number" &&
typeof errorData.total_promotions === "number" &&
typeof errorData.message === "string"
) {
return (
<CreditLimitError
currentBalance={errorData.current_balance}
totalSpent={errorData.total_spent}
totalPromotions={errorData.total_promotions}
message={errorData.message}
/>
)
}
}
{/* {apiProvider === "" && (
<div
style={{
display: "flex",
alignItems: "center",
backgroundColor:
"color-mix(in srgb, var(--vscode-errorForeground) 20%, transparent)",
color: "var(--vscode-editor-foreground)",
padding: "6px 8px",
borderRadius: "3px",
margin: "10px 0 0 0",
fontSize: "12px",
}}>
<i
className="codicon codicon-warning"
style={{
marginRight: 6,
fontSize: 16,
color: "var(--vscode-errorForeground)",
}}></i>
<span>
Uh-oh this could be a problem on end. We've been alerted and
will resolve this ASAP. You can also{" "}
// Default error display
return (
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href=""
style={{ color: "inherit", textDecoration: "underline" }}>
contact us
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{
color: "inherit",
textDecoration: "underline",
}}>
troubleshooting guide
</a>
.
</span>
</div>
)} */}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{
color: "inherit",
textDecoration: "underline",
}}>
troubleshooting guide
</a>
.
</>
)}
</p>
</>
)}
</p>
)
})()}
</>
)}
@@ -1219,3 +1207,20 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
}
}
}
function parseErrorText(text: string | undefined) {
if (!text) {
return undefined
}
try {
const startIndex = text.indexOf("{")
const endIndex = text.lastIndexOf("}")
if (startIndex !== -1 && endIndex !== -1) {
const jsonStr = text.substring(startIndex, endIndex + 1)
const errorObject = JSON.parse(jsonStr)
return errorObject
}
} catch (e) {
// Not JSON or missing required fields
}
}
@@ -742,6 +742,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const unknownModel = "unknown"
if (!apiConfiguration) return unknownModel
switch (selectedProvider) {
case "cline":
return `${selectedProvider}:${selectedModelId}`
case "openai":
return `openai-compat:${selectedModelId}`
case "vscode-lm":
@@ -0,0 +1,41 @@
import React from "react"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
interface CreditLimitErrorProps {
currentBalance: number
totalSpent: number
totalPromotions: number
message: string
}
const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ currentBalance, totalSpent, totalPromotions, message }) => {
return (
<div
style={{
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: "12px",
borderRadius: "4px",
marginBottom: "12px",
}}>
<div style={{ color: "var(--vscode-errorForeground)", marginBottom: "8px" }}>{message}</div>
<div style={{ marginBottom: "12px" }}>
<div style={{ color: "var(--vscode-foreground)" }}>
Current Balance: <span style={{ fontWeight: "bold" }}>${currentBalance.toFixed(2)}</span>
</div>
<div style={{ color: "var(--vscode-foreground)" }}>Total Spent: ${totalSpent.toFixed(2)}</div>
<div style={{ color: "var(--vscode-foreground)" }}>Total Promotions: ${totalPromotions.toFixed(2)}</div>
</div>
<VSCodeButtonLink
href="https://app.cline.bot/credits"
style={{
width: "100%",
}}>
<span className="codicon codicon-credit-card" style={{ fontSize: "14px", marginRight: "6px" }} />
Buy Credits
</VSCodeButtonLink>
</div>
)
}
export default CreditLimitError
@@ -115,7 +115,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
const shouldShowPromptCacheInfo = doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter"
const shouldShowPromptCacheInfo =
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
const ContextWindowComponent = (
<>
@@ -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 ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -48,6 +49,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"
interface ApiOptionsProps {
showModelOptions: boolean
@@ -185,6 +187,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
minWidth: 130,
position: "relative",
}}>
<VSCodeOption value="cline">Cline</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
@@ -206,6 +209,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</VSCodeDropdown>
</DropdownContainer>
{selectedProvider === "cline" && (
<div style={{ marginBottom: 8, marginTop: 4 }}>
<ClineAccountView />
</div>
)}
{selectedProvider === "asksage" && (
<div>
<VSCodeTextField
@@ -1236,6 +1245,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)}
{selectedProvider !== "openrouter" &&
selectedProvider !== "cline" &&
selectedProvider !== "openai" &&
selectedProvider !== "ollama" &&
selectedProvider !== "lmstudio" &&
@@ -1276,7 +1286,9 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</>
)}
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker isPopup={isPopup} />}
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
<OpenRouterModelPicker isPopup={isPopup} />
)}
{modelIdErrorMessage && (
<p
@@ -1482,6 +1494,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "cline":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "openai":
return {
selectedProvider: provider,
@@ -1,4 +1,4 @@
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@@ -9,38 +9,23 @@ import ApiOptions from "../settings/ApiOptions"
const WelcomeView = () => {
const { apiConfiguration } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [email, setEmail] = useState("")
const [isSubscribed, setIsSubscribed] = useState(false)
const [showApiOptions, setShowApiOptions] = useState(false)
const disableLetsGoButton = apiErrorMessage != null
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleSubmit = () => {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
}
const handleSubscribe = () => {
if (email) {
vscode.postMessage({ type: "subscribeEmail", text: email })
}
}
useEffect(() => {
setApiErrorMessage(validateApiConfiguration(apiConfiguration))
}, [apiConfiguration])
// Add message handler for subscription confirmation
const handleMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
if (message.type === "emailSubscribed") {
setIsSubscribed(true)
setEmail("")
}
}, [])
useEvent("message", handleMessage)
return (
<div
style={{
@@ -49,6 +34,9 @@ const WelcomeView = () => {
left: 0,
right: 0,
bottom: 0,
padding: "0 20px",
display: "flex",
flexDirection: "column",
}}>
<div
style={{
@@ -57,59 +45,68 @@ const WelcomeView = () => {
overflow: "auto",
}}>
<h2>Hi, I'm Cline</h2>
<div style={{ display: "flex", justifyContent: "center", margin: "20px 0" }}>
<ClineLogo />
</div>
<p>
I can do all kinds of tasks thanks to breakthroughs in Claude 3.7 Sonnet's agentic coding capabilities and
access to tools that let me create & edit files, explore complex projects, use the browser, and execute
terminal commands (with your permission, of course). I can even use MCP to create new tools and extend my own
capabilities.
I can do all kinds of tasks thanks to breakthroughs in{" "}
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" style={{ display: "inline" }}>
Claude 3.7 Sonnet's
</VSCodeLink>
agentic coding capabilities and access to tools that let me create & edit files, explore complex projects, use
a browser, and execute terminal commands <i>(with your permission, of course)</i>. I can even use MCP to
create new tools and extend my own capabilities.
</p>
<b>To get started, this extension needs an API provider for Claude 3.7 Sonnet.</b>
<p style={{ color: "var(--vscode-descriptionForeground)" }}>
Sign up for an account to get started for free, or use an API key that provides access to models like Claude
3.7 Sonnet.
</p>
<div
style={{
marginTop: "15px",
padding: isSubscribed ? "5px 15px 5px 15px" : "12px",
background: "var(--vscode-textBlockQuote-background)",
borderRadius: "6px",
fontSize: "0.9em",
}}>
{isSubscribed ? (
<p style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ color: "var(--vscode-testing-iconPassed)", fontSize: "1.5em" }}></span>
Thanks for subscribing! We'll keep you updated on new features.
</p>
) : (
<>
<p style={{ margin: 0, marginBottom: "8px" }}>
While Cline currently requires you bring your own API key, we are working on an official accounts
system with additional capabilities. Subscribe to our mailing list to get updates!
</p>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
<VSCodeTextField
type="email"
value={email}
onInput={(e: any) => setEmail(e.target.value)}
placeholder="Enter your email"
style={{ flex: 1 }}
/>
<VSCodeButton appearance="secondary" onClick={handleSubscribe} disabled={!email}>
Subscribe
</VSCodeButton>
</div>
</>
)}
</div>
<VSCodeButton appearance="primary" onClick={handleLogin} style={{ width: "100%" }}>
Get Started for Free
</VSCodeButton>
<div style={{ marginTop: "15px" }}>
<ApiOptions showModelOptions={false} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
{!showApiOptions && (
<VSCodeButton
appearance="secondary"
onClick={() => setShowApiOptions(!showApiOptions)}
style={{ marginTop: 10, width: "100%" }}>
Use your own API key
</VSCodeButton>
)}
<div style={{ marginTop: "18px" }}>
{showApiOptions && (
<div>
<ApiOptions showModelOptions={false} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} style={{ marginTop: "3px" }}>
Let's go!
</VSCodeButton>
</div>
)}
</div>
</div>
</div>
)
}
const ClineLogo: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
// (can't use svgs in vsc extensions)
const logoBase64 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAA8CAYAAAA34qk1AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAOqADAAQAAAABAAAAPAAAAAAs615UAAAGuElEQVRoBd1aW2hcRRj+5yRp4242m0tbSqMitSLaaFGLUXvDB0HUmlYRFXxQQRG0iFXxwctDfWgDKlgREYRWkaJPGsULUjX6IFRbkwZaaL1AUWI1JnvJ7ibpJmf8ZrNnc86ZmXPJZtM9DhzOzH+b/zszZ+af/xxGS1A4543pPD3OON1LjK5Al8txnWFEXzYQ9bW0sLO1dgN91baMTfD1DYzeRS/XaXoawwN4IplgH2j4i0KuKdDUJL+EzdIwPE34ecuJHmpvYQf95BbKrxlQTFeWydPXcOzmgM5lZg3q7oyxPwPKhxIzQkmHEE7naCfEg4IUlpMNJu0J0UUo0ZoBZYzuCOXJnPD2BegEUqkZUPS+NpAHTqEVo6Pc9312qgRr1RLobDAXnFLFFbQgPacVuVU7oJx+kbvzpmBlHFnDWMFbamHcqoFidTVwNbkvZtAnYV3ijD522ym3EVdUV0JtL+jUSE3QpgaDdmDf24qrCwZWwYWqHQkAIyVG3CQaQoDRf26Svli1iuUC6JVEAgMdz/Pt6GAfFK4MarzGcmnY35uM037G2JRfX75Az3Ieby7Qe8TpLj9j54XP6Ffsv72JBDvp1b8n0FyOr55B4A0DG7yM1AEvCx/ubmthh3W+aIHifVyeydEAThs36JTrjJ5t4HSjbmS1qy5Avh4hkOKZtyJW7v+D8wtUA6AEOp7jVwHkIyqFuqZxWpfI05MqH5VTNzPB+7Gn3alSiAAtMztNF3d2MvHeVoo0oqkUbwPI2yoS0askG5fJ/ktAjSa6Hdgao4dv3mMMVO98a64mAQX5WrdQBNtS2kYCirBuTQSBuV2WMEhAoSFi16iX+AjnMTsIFdClCNDtPtSk3vSv86ChAhq24wLSJi+ZJm3Bsp5EdLIe0/9B7FsnwhqC/CET21oTozVFRl2iDtqipEGlfTSd4wMwvg1XkDJIM3RPWxv7zS0sQshsnvoAWrmBO+QZ5SD3QHuc9Tvo5UaqwHcyk95H0zEdVbIWrThJrStXsgmrrRrRZovpc8/rQAo9HJ2mW+P0FKoi5elXntWBFIrtMfYRjojP+Rmx85ubyYHDATSV59dA+Hq7gq6O6bpPNZJ2eYDl2JEftdMU9WPJGL2toDtIrS30JghDDqJHA3GvI4R1AMWZcw90pemssocM/LcqupvW1sx+B+2Mm25rD5QeiI2gqgoZOPadiqekcdptzyhWgGYKvAeGAudicwkaVHagInK9LPr8WaWioiGNErxPos6mGO2y7FSAcpNetogB7jxMtg5Doc3tYGXV8iQ/OFUWF4mnInB62hrVEtDxLN8CuVtUshGndVijWgJqGKFGM1rYy6NqlA7ZwffNaIGc81aM6v0GIpmgwUEUQZZ85py2GlgM/hexrdcoYGVHzj3M3uRlrZ55nL438Dl9CMv/D/XsZ5W+pc0iHZrbR016vkpjdauOUPW1jg6WKQFtT7ABeBok+F4oILwm2jL3sLXseQaMBJYta6VmppCfRqko4jNfmFFl6Sm+dt4F7xocXKeV4HSpludmsBCyQpfRq1baswI0GWNH4NCnbtvadpE2ank2Bs6lIqO4wUZyVNFnIDslJRZClmisWKD9VmcVoIKAoPlF3HAG9i+YAbvE91I/yWyOHoOM42zo0tkeZHZkMvwyeBYm3/yK9uCNFfg4jB1xOaJsYhPejP+InlEyy8Rsll+OFb3PSwa8OA7wB/DQmnRy4C0zG+kg+IEzDIZJ79jtySPCaNou4FPvy+T4gfFxnnTL4RD/sGnQj6AHcW4rHtpPqRyXprhIBoB3FEf4m9x9eLWnp5048Io4S8ickaWcxzI+iDk/jNzOhVgENqIu5VYtYY873h46CaeOlWWEHfGTpDwgHkYEy50zWqxPD3ExlWF/s0gDVFEEoG6A667ChlJV9aRmlZIRI7r/V1IB/TtimGR3kbVwZ0AkoJh5f8maEaNwGnF7LAGFwFG3UATb1mJWcV0COnuOPge3WJGIYgU/XLndloCKSB9Bw2duwQi1U8Wp0mA5XJaACi6W3RdwE3taFMtee+hnAVAC7UywE9gP37KEInQ/jV/m3lD5q93eRXyZLtA3CL02qRTrkJZBfNvT2spOqXxTjqgQxLeOc41m6f8/aQVTGTrPtDQisx06kMI3LVDBxO9m/0zEaQuqH4p2nZZTYiTLWRKti55AhdZFjE3iZ8L7MMdvRXNYa2npGeOIiXfjnbwaI3nar3vtO6pSxHvLspPUgw9SvdiCtuGU0gW51biWqeQXkSZ2gFFcI3D4OHLR/ZMx+sod5nn19x8Bu+YF5eP/fAAAAABJRU5ErkJggg=="
return (
<img
src={logoBase64}
style={{
width: "57px",
height: "60px",
...style,
}}
alt="Cline Logo"
/>
)
}
export default WelcomeView
@@ -39,7 +39,6 @@ export const ExtensionStateContextProvider: React.FC<{
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
browserSettings: DEFAULT_BROWSER_SETTINGS,
chatSettings: DEFAULT_CHAT_SETTINGS,
isLoggedIn: false,
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
vscMachineId: "",
@@ -79,6 +78,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.qwenApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
].some((key) => key !== undefined)
@@ -0,0 +1,105 @@
import { User, getAuth, signInWithCustomToken, signOut } from "firebase/auth"
import { initializeApp } from "firebase/app"
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { vscode } from "../utils/vscode"
// Firebase configuration from extension
const firebaseConfig = {
apiKey: "AIzaSyDcXAaanNgR2_T0dq2oOl5XyKPksYHppVo",
authDomain: "cline-bot.firebaseapp.com",
projectId: "cline-bot",
storageBucket: "cline-bot.firebasestorage.app",
messagingSenderId: "364369702101",
appId: "1:364369702101:web:0013885dcf20b43799c65c",
measurementId: "G-MDPRELSCD1",
}
interface FirebaseAuthContextType {
user: User | null
isInitialized: boolean
signInWithToken: (token: string) => Promise<void>
handleSignOut: () => Promise<void>
}
const FirebaseAuthContext = createContext<FirebaseAuthContextType | undefined>(undefined)
export const FirebaseAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null)
const [isInitialized, setIsInitialized] = useState(false)
// Initialize Firebase
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
// Handle auth state changes
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user)
setIsInitialized(true)
// Sync auth state with extension
vscode.postMessage({
type: "authStateChanged",
user: user
? {
displayName: user.displayName,
email: user.email,
photoURL: user.photoURL,
}
: null,
})
})
return () => unsubscribe()
}, [auth])
const signInWithToken = useCallback(
async (token: string) => {
try {
await signInWithCustomToken(auth, token)
console.log("Successfully signed in with custom token")
} catch (error) {
console.error("Error signing in with custom token:", error)
throw error
}
},
[auth],
)
// Listen for auth callback from extension
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "authCallback" && message.customToken) {
signInWithToken(message.customToken)
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [signInWithToken])
const handleSignOut = useCallback(async () => {
try {
await signOut(auth)
console.log("Successfully signed out of Firebase")
} catch (error) {
console.error("Error signing out of Firebase:", error)
throw error
}
}, [auth])
return (
<FirebaseAuthContext.Provider value={{ user, isInitialized, signInWithToken, handleSignOut }}>
{children}
</FirebaseAuthContext.Provider>
)
}
export const useFirebaseAuth = () => {
const context = useContext(FirebaseAuthContext)
if (context === undefined) {
throw new Error("useFirebaseAuth must be used within a FirebaseAuthProvider")
}
return context
}
+6
View File
@@ -53,6 +53,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."
@@ -100,6 +105,7 @@ export function validateModelId(
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "openrouter":
case "cline":
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!modelId) {
return "You must provide a model ID."