mirror of
https://github.com/cline/cline.git
synced 2026-09-16 06:32:31 +08:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9504e965 | ||
|
|
de3581f2d2 | ||
|
|
563810fc60 | ||
|
|
76e1b68115 | ||
|
|
17da90ff47 | ||
|
|
477e2a2695 | ||
|
|
3ecabb559e | ||
|
|
8f8de75dc8 | ||
|
|
78603bfd6b | ||
|
|
89ad9a241c | ||
|
|
53582d6149 | ||
|
|
ac53b9e66b | ||
|
|
b8c5aeaff5 | ||
|
|
7ae6c0cfa3 | ||
|
|
28fb2ae69c | ||
|
|
f05d3e9295 | ||
|
|
29c16ea7c8 | ||
|
|
20bab1edfa | ||
|
|
8755bf9805 | ||
|
|
24a38e1471 | ||
|
|
4b12f2aaca | ||
|
|
c7ac62ad21 | ||
|
|
32dcdcf038 | ||
|
|
f7ee18cd88 | ||
|
|
0f7b726b70 | ||
|
|
2fde7642c2 | ||
|
|
82d9385b8b | ||
|
|
5a3a9f1392 | ||
|
|
958b28658b | ||
|
|
8ab768e770 | ||
|
|
03af507fe6 | ||
|
|
b1cd61d909 | ||
|
|
b7d2ce9429 | ||
|
|
11bc4e6f94 | ||
|
|
b504224ab7 | ||
|
|
787863f7b4 | ||
|
|
289ca46515 | ||
|
|
914e574be3 | ||
|
|
5625a93fcc | ||
|
|
5b3b710003 | ||
|
|
1e735da806 | ||
|
|
0c8df6aa61 | ||
|
|
c06e91d459 | ||
|
|
57e64ba50c | ||
|
|
d4da786018 | ||
|
|
dcc06b2e3d | ||
|
|
56de6dfc4e | ||
|
|
db294ff45a | ||
|
|
5c59f19fbf | ||
|
|
3c69365f7a | ||
|
|
82f0620ab7 | ||
|
|
199bdedbed | ||
|
|
1b59a33880 | ||
|
|
6e65b869ae | ||
|
|
cf61a8961a |
@@ -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"
|
||||
@@ -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":
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface UserInfo {
|
||||
displayName: string | null
|
||||
email: string | null
|
||||
photoURL: string | null
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1002
-7
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user