mirror of
https://github.com/cline/cline.git
synced 2026-09-16 21:01:52 +08:00
resolving merge conflicts
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed problem with “win+shift+a” shortcut not working in Windows
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Better Openrouter error typing and throwing more detailed messages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add retries to Bedrock createMessage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add webview to lint job
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Update Claude 3.5 -> Claude 3.7 in error message/recommendation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add support for AskSage as model provider.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add timeout option to MCP servers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add APAC Support for Cross-Region Inference Profiles
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash
|
||||
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [3.5.1]
|
||||
|
||||
- Add timeout option to MCP servers
|
||||
- Add Gemini Flash models to Vertex provider (thanks @jpaodev!)
|
||||
- Add prompt caching support for AWS Bedrock provider (thanks @buger!)
|
||||
- Add AskSage provider (thanks @swhite24!)
|
||||
|
||||
## [3.5.0]
|
||||
|
||||
- Add 'Enable extended thinking' option for Claude 3.7 Sonnet, with ability to set different budgets for Plan and Act modes
|
||||
|
||||
Generated
+31
-2
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
@@ -36,6 +37,7 @@
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
@@ -3787,6 +3789,18 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="
|
||||
},
|
||||
"node_modules/@google-cloud/vertexai": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz",
|
||||
"integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.18.0.tgz",
|
||||
@@ -11051,6 +11065,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.13.tgz",
|
||||
"integrity": "sha512-qK3eE2GjMYjCiTknEJfAHjbUzUqgVtf9qtzjxWrkwBZgBG7kOB6Z4+Ov4fbvDjmKKHv+rpuTsWFg4jZvVjNBtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -13401,6 +13424,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
|
||||
+4
-2
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
@@ -236,7 +236,7 @@
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
@@ -275,6 +275,7 @@
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
@@ -299,6 +300,7 @@
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
|
||||
@@ -19,11 +19,13 @@ export class AnthropicHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0 ? true : false
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
const modelId = model.id
|
||||
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
|
||||
@@ -16,11 +16,12 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0 ? true : false
|
||||
// cross region inference requires prefixing the model id with the region
|
||||
let modelId = await this.getModelId()
|
||||
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
// Get model info and message indices for caching
|
||||
const model = this.getModel()
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
|
||||
@@ -33,11 +34,47 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
}),
|
||||
stream: true,
|
||||
})
|
||||
|
||||
|
||||
+12
-17
@@ -1,40 +1,35 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { Message, Ollama } from "ollama"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class OllamaHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: Ollama
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
|
||||
apiKey: "ollama",
|
||||
})
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await this.client.chat({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
messages: ollamaMessages,
|
||||
stream: true,
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
|
||||
},
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
text: chunk.message.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class OpenAiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -32,20 +33,30 @@ export class OpenAiHandler implements ApiHandler {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.openAiModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
const isO3Mini = modelId.includes("o3-mini")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
let temperature: number | undefined = 0
|
||||
let reasoningEffort: ChatCompletionReasoningEffort | undefined = undefined
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
if (isO3Mini) {
|
||||
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
temperature = undefined // does not support temperature
|
||||
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
temperature,
|
||||
reasoning_effort: reasoningEffort,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
|
||||
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
export class OpenRouterHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// For the following openrouter error type sources, see the docs here:
|
||||
// https://openrouter.ai/docs/api-reference/errors
|
||||
|
||||
export type OpenRouterErrorResponse = {
|
||||
error: {
|
||||
message: string
|
||||
code: number
|
||||
metadata?: OpenRouterProviderErrorMetadata | OpenRouterModerationErrorMetadata | Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenRouterProviderErrorMetadata = {
|
||||
provider_name: string // The name of the provider that encountered the error
|
||||
raw: unknown // The raw error from the provider
|
||||
}
|
||||
|
||||
export type OpenRouterModerationErrorMetadata = {
|
||||
reasons: string[] // Why your input was flagged
|
||||
flagged_input: string // The text segment that was flagged, limited to 100 characters. If the flagged input is longer than 100 characters, it will be truncated in the middle and replaced with ...
|
||||
provider_name: string // The name of the provider that requested moderation
|
||||
model_slug: string
|
||||
}
|
||||
+199
-136
@@ -4,19 +4,25 @@ import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { VertexAI } from "@google-cloud/vertexai"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class VertexHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: AnthropicVertex
|
||||
private clientAnthropic: AnthropicVertex
|
||||
private clientVertex: VertexAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new AnthropicVertex({
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
})
|
||||
this.clientVertex = new VertexAI({
|
||||
project: this.options.vertexProjectId,
|
||||
location: this.options.vertexRegion,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
@@ -24,40 +30,66 @@ export class VertexHandler implements ApiHandler {
|
||||
const model = this.getModel()
|
||||
const modelId = model.id
|
||||
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0 ? true : false
|
||||
if (modelId.includes("claude")) {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
let stream
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
// Find indices of user messages for cache control
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
let stream
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
// Find indices of user messages for cache control
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
stream = await this.client.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
stream = await this.clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
@@ -66,121 +98,152 @@ export class VertexHandler implements ApiHandler {
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
: message.content,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
}
|
||||
}),
|
||||
stream: true,
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.client.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
}),
|
||||
stream: true,
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.clientAnthropic.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// gemini
|
||||
const generativeModel = this.clientVertex.getGenerativeModel({
|
||||
model: this.getModel().id,
|
||||
systemInstruction: {
|
||||
role: "system",
|
||||
parts: [{ text: systemPrompt }],
|
||||
},
|
||||
})
|
||||
const request = {
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: messages.map((m) => {
|
||||
if (typeof m.content === "string") {
|
||||
return { text: m.content }
|
||||
} else if (Array.isArray(m.content)) {
|
||||
return {
|
||||
text: m.content
|
||||
.map((block) => {
|
||||
if (typeof block === "string") {
|
||||
return block
|
||||
} else if (block.type === "text") {
|
||||
return block.text
|
||||
} else {
|
||||
console.log("Unsupported block type", block)
|
||||
return ""
|
||||
}
|
||||
})
|
||||
.join(" "),
|
||||
}
|
||||
} else {
|
||||
return { text: "" }
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
const streamingResult = await generativeModel.generateContentStream(request)
|
||||
for await (const chunk of streamingResult.stream) {
|
||||
// If usage data is available, yield it similarly:
|
||||
// yield { type: "usage", inputTokens: 0, outputTokens: 0 }
|
||||
// Otherwise, just yield text:
|
||||
const candidates = chunk.candidates || []
|
||||
for (const candidate of candidates) {
|
||||
for (const part of candidate.content?.parts || []) {
|
||||
if (part.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
text: part.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Message } from "ollama"
|
||||
|
||||
export function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
|
||||
const ollamaMessages: Message[] = []
|
||||
|
||||
for (const anthropicMessage of anthropicMessages) {
|
||||
if (typeof anthropicMessage.content === "string") {
|
||||
ollamaMessages.push({
|
||||
role: anthropicMessage.role,
|
||||
content: anthropicMessage.content,
|
||||
})
|
||||
} else {
|
||||
if (anthropicMessage.role === "user") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolResultBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_result") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process tool result messages FIRST since they must follow the tool use messages
|
||||
let toolResultImages: string[] = []
|
||||
toolMessages.forEach((toolMessage) => {
|
||||
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
|
||||
let content: string
|
||||
|
||||
if (typeof toolMessage.content === "string") {
|
||||
content = toolMessage.content
|
||||
} else {
|
||||
content =
|
||||
toolMessage.content
|
||||
?.map((part) => {
|
||||
if (part.type === "image") {
|
||||
toolResultImages.push(`data:${part.source.media_type};base64,${part.source.data}`)
|
||||
return "(see following user message for image)"
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n") ?? ""
|
||||
}
|
||||
ollamaMessages.push({
|
||||
role: "user",
|
||||
images: toolResultImages.length > 0 ? toolResultImages : undefined,
|
||||
content: content,
|
||||
})
|
||||
})
|
||||
|
||||
// Process non-tool messages
|
||||
if (nonToolMessages.length > 0) {
|
||||
ollamaMessages.push({
|
||||
role: "user",
|
||||
content: nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return `data:${part.source.media_type};base64,${part.source.data}`
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n"),
|
||||
})
|
||||
}
|
||||
} else if (anthropicMessage.role === "assistant") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolUseBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_use") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
} // assistant cannot send tool_result messages
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process non-tool messages
|
||||
let content: string = ""
|
||||
if (nonToolMessages.length > 0) {
|
||||
content = nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return "" // impossible as the assistant cannot send images
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
ollamaMessages.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ollamaMessages
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -146,9 +147,11 @@ export async function* streamOpenRouterFormatRequest(
|
||||
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 { message?: string; code?: number }
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
throw new 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) {
|
||||
|
||||
@@ -79,6 +79,7 @@ type GlobalStateKey =
|
||||
| "openAiModelInfo"
|
||||
| "ollamaModelId"
|
||||
| "ollamaBaseUrl"
|
||||
| "ollamaApiOptionsCtxNum"
|
||||
| "lmStudioModelId"
|
||||
| "lmStudioBaseUrl"
|
||||
| "anthropicBaseUrl"
|
||||
@@ -579,6 +580,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
@@ -624,6 +626,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||
@@ -1901,6 +1904,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
@@ -1959,6 +1963,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("ollamaApiOptionsCtxNum") as Promise<string | undefined>,
|
||||
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
|
||||
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
|
||||
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
|
||||
@@ -2040,6 +2045,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ApiHandlerOptions {
|
||||
openAiModelInfo?: ModelInfo
|
||||
ollamaModelId?: string
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiOptionsCtxNum?: string
|
||||
lmStudioModelId?: string
|
||||
lmStudioBaseUrl?: string
|
||||
geminiApiKey?: string
|
||||
@@ -298,6 +299,78 @@ export const vertexModels = {
|
||||
cacheWritesPrice: 0.3,
|
||||
cacheReadsPrice: 0.03,
|
||||
},
|
||||
"gemini-2.0-flash-001": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.4,
|
||||
},
|
||||
"gemini-2.0-flash-thinking-exp-1219": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32_767,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.0-flash-exp": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-exp-1206": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-8b-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-pro-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-pro-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const openAiModelInfoSaneDefaults: ModelInfo = {
|
||||
|
||||
@@ -815,7 +815,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
{isExpanded ? (
|
||||
<div style={{ marginTop: -3 }}>
|
||||
<span style={{ fontWeight: "bold", display: "block", marginBottom: "4px" }}>
|
||||
Reasoning
|
||||
Thinking
|
||||
<span
|
||||
className="codicon codicon-chevron-down"
|
||||
style={{
|
||||
|
||||
@@ -1173,6 +1173,13 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
|
||||
placeholder={"e.g. llama3.1"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
|
||||
placeholder={"e.g. 32768"}>
|
||||
<span style={{ fontWeight: 500 }}>Model Context Window</span>
|
||||
</VSCodeTextField>
|
||||
{ollamaModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
|
||||
@@ -59,7 +59,7 @@ const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--vscode-progressBar-background);
|
||||
border: 0px solid var(--vscode-progressBar-background);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,9 @@ export function getContextMenuOptions(
|
||||
const seen = new Set()
|
||||
const deduped = allItems.filter((item) => {
|
||||
const key = `${item.type}-${item.value}`
|
||||
if (seen.has(key)) return false
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
@@ -195,18 +197,26 @@ export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
const beforeCursor = text.slice(0, position)
|
||||
const atIndex = beforeCursor.lastIndexOf("@")
|
||||
|
||||
if (atIndex === -1) return false
|
||||
if (atIndex === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const textAfterAt = beforeCursor.slice(atIndex + 1)
|
||||
|
||||
// Check if there's any whitespace after the '@'
|
||||
if (/\s/.test(textAfterAt)) return false
|
||||
if (/\s/.test(textAfterAt)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show the menu if it's a URL
|
||||
if (textAfterAt.toLowerCase().startsWith("http")) return false
|
||||
if (textAfterAt.toLowerCase().startsWith("http")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't show the menu if it's a problems or terminal
|
||||
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) return false
|
||||
if (textAfterAt.toLowerCase().startsWith("problems") || textAfterAt.toLowerCase().startsWith("terminal")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ export const useShortcut = (shortcut: string, callback: any, options = { disable
|
||||
|
||||
if (Object.keys(modifierMap).includes(keyArray[0])) {
|
||||
const finalKey = keyArray.pop()
|
||||
if (!finalKey) return
|
||||
|
||||
if (keyArray.every((k) => modifierMap[k]) && finalKey === event.key) {
|
||||
if (keyArray.every((k) => modifierMap[k]) && finalKey.toLowerCase() === event.key.toLowerCase()) {
|
||||
event.preventDefault()
|
||||
return callbackRef.current(event)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -35,7 +35,9 @@ export function findMatchingResourceOrTemplate(
|
||||
): McpResource | McpResourceTemplate | undefined {
|
||||
// First try to find an exact resource match
|
||||
const exactMatch = resources.find((resource) => resource.uri === uri)
|
||||
if (exactMatch) return exactMatch
|
||||
if (exactMatch) {
|
||||
return exactMatch
|
||||
}
|
||||
|
||||
// If no exact match, try to find a matching template
|
||||
return findMatchingTemplate(uri, templates)
|
||||
|
||||
@@ -36,7 +36,5 @@ export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
|
||||
|
||||
// We want to re‐schedule if any item in `deps` changed,
|
||||
// or if `delay` changed.
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [delay, ...deps])
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ export function hexToRGB(hexColor: string): { r: number; g: number; b: number }
|
||||
|
||||
export function colorToHex(colorVar: string): string {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(colorVar).trim()
|
||||
if (value.startsWith("#")) return value.slice(0, 7)
|
||||
if (value.startsWith("#")) {
|
||||
return value.slice(0, 7)
|
||||
}
|
||||
|
||||
const rgbValues = value.match(/\d+/g)?.slice(0, 3).map(Number) || []
|
||||
return `#${rgbValues.map((x) => x.toString(16).padStart(2, "0")).join("")}`
|
||||
|
||||
Reference in New Issue
Block a user