+
No model found.
@@ -85,13 +93,30 @@ export const ModelPicker = ({
- {selectedModelId && selectedModelInfo && (
-
+ {errorMessage ? (
+
+
+
+ Note: Roo Code uses complex prompts and works best
+ with Claude models. Less capable models may not work as expected.
+
+
+
+ ) : (
+ selectedModelId &&
+ selectedModelInfo && (
+
+ )
)}
The extension automatically fetches the latest list of models available on{" "}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 75ba11107c..ee032c3ee0 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -1,6 +1,6 @@
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { Dropdown, type DropdownOption } from "vscrui"
+import { Button, Dropdown, type DropdownOption } from "vscrui"
import {
AlertDialog,
@@ -14,7 +14,6 @@ import {
} from "@/components/ui"
import { vscode } from "../../utils/vscode"
-import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
import { ApiConfiguration } from "../../../../src/shared/api"
@@ -33,14 +32,13 @@ export interface SettingsViewRef {
const SettingsView = forwardRef(({ onDone }, ref) => {
const extensionState = useExtensionState()
- const [apiErrorMessage, setApiErrorMessage] = useState(undefined)
- const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined)
const [commandInput, setCommandInput] = useState("")
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
const [cachedState, setCachedState] = useState(extensionState)
const [isChangeDetected, setChangeDetected] = useState(false)
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
const confirmDialogHandler = useRef<() => void>()
+ const [errorMessage, setErrorMessage] = useState(undefined)
// TODO: Reduce WebviewMessage/ExtensionState complexity
const { currentApiConfigName } = extensionState
@@ -135,20 +133,9 @@ const SettingsView = forwardRef(({ onDone },
}
})
}, [])
-
+ const isSettingValid = !errorMessage
const handleSubmit = () => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
-
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
-
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
-
- if (!apiValidationResult && !modelIdValidationResult) {
+ if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -177,23 +164,6 @@ const SettingsView = forwardRef(({ onDone },
}
}
- useEffect(() => {
- setApiErrorMessage(undefined)
- setModelIdErrorMessage(undefined)
- }, [apiConfiguration])
-
- // Initial validation on mount
- useEffect(() => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
- }, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
-
const checkUnsaveChanges = useCallback(
(then: () => void) => {
if (isChangeDetected) {
@@ -287,13 +257,14 @@ const SettingsView = forwardRef(({ onDone },
justifyContent: "space-between",
gap: "6px",
}}>
-
+ disabled={!isChangeDetected || !isSettingValid}>
Save
-
+
(({ onDone },
uriScheme={extensionState.uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
- apiErrorMessage={apiErrorMessage}
- modelIdErrorMessage={modelIdErrorMessage}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 8f2d0dff89..73394bae10 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,6 +51,8 @@ describe("ApiOptions", () => {
render(
{}}
uriScheme={undefined}
apiConfiguration={{}}
setApiConfigurationField={() => {}}
@@ -69,4 +71,6 @@ describe("ApiOptions", () => {
renderApiOptions({ fromWelcomeView: true })
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
})
+
+ //TODO: More test cases needed
})
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 858d2622f3..5d880efc0b 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -42,6 +42,8 @@ const WelcomeView = () => {
apiConfiguration={apiConfiguration || {}}
uriScheme={uriScheme}
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
From 48975003afe593c975476acfd348ceb6110d7ced Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 06:50:11 +0800
Subject: [PATCH 030/298] Remove ModelInfo related exports from
ExtensionStateContext
---
.../src/context/ExtensionStateContext.tsx | 84 +------------------
1 file changed, 1 insertion(+), 83 deletions(-)
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index 3dca8d5f51..c2c4d181e4 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -1,18 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
-import {
- ApiConfiguration,
- ModelInfo,
- glamaDefaultModelId,
- glamaDefaultModelInfo,
- openRouterDefaultModelId,
- openRouterDefaultModelInfo,
- unboundDefaultModelId,
- unboundDefaultModelInfo,
- requestyDefaultModelId,
- requestyDefaultModelInfo,
-} from "../../../src/shared/api"
+import { ApiConfiguration } from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
@@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: any
- glamaModels: Record
- requestyModels: Record
- openRouterModels: Record
- unboundModels: Record
- openAiModels: string[]
mcpServers: McpServer[]
currentCheckpoint?: string
filePaths: string[]
@@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setRateLimitSeconds: (value: number) => void
setCurrentApiConfigName: (value: string) => void
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
- onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
mode: Mode
setMode: (value: Mode) => void
setCustomModePrompts: (value: CustomModePrompts) => void
@@ -124,21 +107,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [showWelcome, setShowWelcome] = useState(false)
const [theme, setTheme] = useState(undefined)
const [filePaths, setFilePaths] = useState([])
- const [glamaModels, setGlamaModels] = useState>({
- [glamaDefaultModelId]: glamaDefaultModelInfo,
- })
const [openedTabs, setOpenedTabs] = useState>([])
- const [openRouterModels, setOpenRouterModels] = useState>({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo,
- })
- const [unboundModels, setUnboundModels] = useState>({
- [unboundDefaultModelId]: unboundDefaultModelInfo,
- })
- const [requestyModels, setRequestyModels] = useState>({
- [requestyDefaultModelId]: requestyDefaultModelInfo,
- })
- const [openAiModels, setOpenAiModels] = useState([])
const [mcpServers, setMcpServers] = useState([])
const [currentCheckpoint, setCurrentCheckpoint] = useState()
@@ -146,18 +116,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
[],
)
-
- const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
- setState((currentState) => {
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentState.currentApiConfigName,
- apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig },
- })
- return currentState // No state update needed
- })
- }, [])
-
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -202,40 +160,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
})
break
}
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(updatedModels)
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
@@ -264,11 +188,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
didHydrateState,
showWelcome,
theme,
- glamaModels,
- requestyModels,
- openRouterModels,
- openAiModels,
- unboundModels,
mcpServers,
currentCheckpoint,
filePaths,
@@ -316,7 +235,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
setListApiConfigMeta,
- onUpdateApiConfig,
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
From 05151ed2254e8698d4c380ceb8daa36e019f82aa Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Tue, 25 Feb 2025 18:30:37 -0500
Subject: [PATCH 031/298] Update package.json
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 84bec2645a..28045436e6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "roo-cline",
"displayName": "Roo Code (prev. Roo Cline)",
- "description": "An AI-powered autonomous coding agent that lives in your editor.",
+ "description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
"version": "3.7.4",
"icon": "assets/icons/rocket.png",
From e56908f6c1b800bd2a2b4edd85f725c1a0055920 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:48:20 -0800
Subject: [PATCH 032/298] Thinking settings tweaks
---
src/api/providers/anthropic.ts | 23 +++++----
src/api/providers/openrouter.ts | 12 ++++-
src/shared/api.ts | 14 +++++-
.../src/components/settings/ApiOptions.tsx | 50 ++++++-------------
4 files changed, 51 insertions(+), 48 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 2d1f07f833..c907350607 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -14,8 +14,6 @@ import { ApiStream } from "../transform/stream"
const ANTHROPIC_DEFAULT_TEMPERATURE = 0
-const THINKING_MODELS = ["claude-3-7-sonnet-20250219"]
-
export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private client: Anthropic
@@ -32,16 +30,19 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- const modelId = this.getModel().id
- const maxTokens = this.getModel().info.maxTokens || 8192
+ let { id: modelId, info: modelInfo } = this.getModel()
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
- if (THINKING_MODELS.includes(modelId)) {
- thinking = this.options.anthropicThinking
- ? { type: "enabled", budget_tokens: this.options.anthropicThinking }
- : { type: "disabled" }
-
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ if (modelId === "claude-3-7-sonnet-20250219:thinking") {
+ // The `:thinking` variant is a virtual identifier for the
+ // `claude-3-7-sonnet-20250219` model with a thinking budget.
+ // We can handle this more elegantly in the future.
+ modelId = "claude-3-7-sonnet-20250219"
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -114,8 +115,8 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
default: {
stream = (await this.client.messages.create({
model: modelId,
- max_tokens: this.getModel().info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
+ max_tokens: maxTokens,
+ temperature,
system: [{ text: systemPrompt, type: "text" }],
messages,
// tools,
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69c55b8e71..6bf4fa4a8c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -1,4 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import axios from "axios"
import OpenAI from "openai"
import delay from "delay"
@@ -17,6 +18,7 @@ const OPENROUTER_DEFAULT_TEMPERATURE = 0
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
+ thinking?: BetaThinkingConfigParam
}
// Add custom interface for OpenRouter usage chunk.
@@ -57,7 +59,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (true) {
- case this.getModel().id.startsWith("anthropic/"):
+ case modelId.startsWith("anthropic/"):
openAiMessages[0] = {
role: "system",
content: [
@@ -108,8 +110,13 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
let temperature = this.options.modelTemperature ?? defaultTemperature
+ const maxTokens = modelInfo.maxTokens
+ const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
// Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -118,8 +125,9 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: modelInfo.maxTokens,
+ max_tokens: maxTokens,
temperature,
+ thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
messages: openAiMessages,
stream: true,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 63707e52b4..5d4b8b120d 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -103,7 +103,7 @@ export const THINKING_BUDGET = {
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicModels = {
- "claude-3-7-sonnet-20250219": {
+ "claude-3-7-sonnet-20250219:thinking": {
maxTokens: 16384,
contextWindow: 200_000,
supportsImages: true,
@@ -115,6 +115,18 @@ export const anthropicModels = {
cacheReadsPrice: 0.3, // $0.30 per million tokens
thinking: true,
},
+ "claude-3-7-sonnet-20250219": {
+ maxTokens: 16384,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million input tokens
+ outputPrice: 15.0, // $15 per million output tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ thinking: false,
+ },
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200_000,
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 9d17cae4fa..73dc4fd41f 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -73,7 +73,7 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking
+ const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
@@ -1272,39 +1272,21 @@ const ApiOptions = ({
)}
{selectedModelInfo && selectedModelInfo.thinking && (
-
-
- setApiConfigurationField(
- "anthropicThinking",
- checked
- ? Math.min(
- THINKING_BUDGET.default,
- selectedModelInfo.maxTokens ?? THINKING_BUDGET.default,
- )
- : undefined,
- )
- }>
- Thinking?
-
- {anthropicThinkingBudget && (
- <>
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- >
- )}
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {anthropicThinkingBudget}
+
+
+ Number of tokens Claude is allowed to use for its internal reasoning process.
+
)}
From 8971e47b96ec7ae3a6de4ec5a95a2acab4cba1b7 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:53:32 -0800
Subject: [PATCH 033/298] Add changeset
---
.changeset/swift-kings-attack.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
new file mode 100644
index 0000000000..8a8a425611
--- /dev/null
+++ b/.changeset/swift-kings-attack.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Pass "thinking" params to OpenRouter
From 33fd3bd6b35caafce66fcd53b9070f60279fcc0d Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 16:26:40 -0800
Subject: [PATCH 034/298] Fix budgetTokens
---
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 8 +++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index c907350607..ad58a1cf6b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -32,7 +32,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, info: modelInfo } = this.getModel()
const maxTokens = modelInfo.maxTokens || 8192
- const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
@@ -42,6 +41,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// `claude-3-7-sonnet-20250219` model with a thinking budget.
// We can handle this more elegantly in the future.
modelId = "claude-3-7-sonnet-20250219"
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 6bf4fa4a8c..0a9488e816 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -109,13 +109,11 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}
let temperature = this.options.modelTemperature ?? defaultTemperature
-
- const maxTokens = modelInfo.maxTokens
- const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
let thinking: BetaThinkingConfigParam | undefined = undefined
- // Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -125,7 +123,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: maxTokens,
+ max_tokens: modelInfo.maxTokens,
temperature,
thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
From f3d02030ac47420d8a9735c0d02f70e561475dae Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 08:37:28 +0800
Subject: [PATCH 035/298] Fix: Input/output prices should be parsed using
parseFloat
---
webview-ui/src/components/settings/ApiOptions.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 73dc4fd41f..bfcf93256e 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -900,7 +900,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ??
openAiModelInfoSaneDefaults),
@@ -945,7 +945,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
From 41e75bc9890036674cd31ce2d9da23fcd5127956 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:02:43 -0800
Subject: [PATCH 036/298] Model picker fixes
---
.changeset/real-ties-destroy.md | 5 +
src/api/providers/requesty.ts | 36 +-
src/core/webview/ClineProvider.ts | 22 +-
src/shared/ExtensionMessage.ts | 13 +-
src/shared/WebviewMessage.ts | 5 +-
webview-ui/package-lock.json | 2 +
.../components/settings/ApiErrorMessage.tsx | 18 +-
.../src/components/settings/ApiOptions.tsx | 445 ++++++++----------
.../src/components/settings/ModelPicker.tsx | 91 ++--
.../src/components/settings/SettingsView.tsx | 48 +-
.../components/settings/ThinkingBudget.tsx | 29 ++
webview-ui/src/components/ui/alert-dialog.tsx | 151 +++---
webview-ui/src/components/ui/dialog.tsx | 158 ++++---
.../src/components/welcome/WelcomeView.tsx | 8 +-
webview-ui/src/utils/validate.ts | 254 ++++++----
15 files changed, 664 insertions(+), 621 deletions(-)
create mode 100644 .changeset/real-ties-destroy.md
create mode 100644 webview-ui/src/components/settings/ThinkingBudget.tsx
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
new file mode 100644
index 0000000000..a2e9ba8eb0
--- /dev/null
+++ b/.changeset/real-ties-destroy.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Fix model picker
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 2151a7172d..5e570ca2a2 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler {
}
}
-export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
+export async function getRequestyModels() {
const models: Record = {}
- if (!apiKey) {
- return models
- }
-
try {
- const config: Record = {}
- config["headers"] = { Authorization: `Bearer ${apiKey}` }
-
- const response = await axios.get("https://router.requesty.ai/v1/models", config)
+ const response = await axios.get("https://router.requesty.ai/v1/models")
const rawModels = response.data.data
for (const rawModel of rawModels) {
+ // {
+ // id: "anthropic/claude-3-5-sonnet-20240620",
+ // object: "model",
+ // created: 1740552655,
+ // owned_by: "system",
+ // input_price: 0.0000028,
+ // caching_price: 0.00000375,
+ // cached_price: 3e-7,
+ // output_price: 0.000015,
+ // max_output_tokens: 8192,
+ // context_window: 200000,
+ // supports_caching: true,
+ // description:
+ // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.",
+ // }
+
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_output_tokens,
contextWindow: rawModel.context_window,
- supportsImages: rawModel.support_image,
- supportsComputerUse: rawModel.support_computer_use,
supportsPromptCache: rawModel.supports_caching,
inputPrice: parseApiPrice(rawModel.input_price),
outputPrice: parseApiPrice(rawModel.output_price),
@@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
modelInfo.maxTokens = 16384
break
+ case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
+ modelInfo.maxTokens = 8192
+ break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 118bbddcf5..bc6f457868 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
- const requestyApiKey = await this.getSecret("requestyApiKey")
-
- getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => {
+ getRequestyModels().then(async (requestyModels) => {
if (Object.keys(requestyModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
@@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "refreshRequestyModels":
- if (message?.values?.apiKey) {
- const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey })
+ const requestyModels = await getRequestyModels()
- if (Object.keys(requestyModels).length > 0) {
- const cacheDir = await this.ensureCacheDirectoryExists()
- await fs.writeFile(
- path.join(cacheDir, GlobalFileNames.requestyModels),
- JSON.stringify(requestyModels),
- )
- await this.postMessageToWebview({ type: "requestyModels", requestyModels })
- }
+ if (Object.keys(requestyModels).length > 0) {
+ const cacheDir = await this.ensureCacheDirectoryExists()
+ await fs.writeFile(
+ path.join(cacheDir, GlobalFileNames.requestyModels),
+ JSON.stringify(requestyModels),
+ )
+ await this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
break
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 8f64a9ba05..e87edffed1 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -27,10 +27,11 @@ export interface ExtensionMessage {
| "workspaceUpdated"
| "invoke"
| "partialMessage"
- | "glamaModels"
| "openRouterModels"
- | "openAiModels"
+ | "glamaModels"
+ | "unboundModels"
| "requestyModels"
+ | "openAiModels"
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
@@ -43,8 +44,6 @@ export interface ExtensionMessage {
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
- | "unboundModels"
- | "refreshUnboundModels"
| "currentCheckpointUpdated"
text?: string
action?:
@@ -67,11 +66,11 @@ export interface ExtensionMessage {
path?: string
}>
partialMessage?: ClineMessage
- glamaModels?: Record
- requestyModels?: Record
openRouterModels?: Record
- openAiModels?: string[]
+ glamaModels?: Record
unboundModels?: Record
+ requestyModels?: Record
+ openAiModels?: string[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 106e6d243b..fde7442cc1 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -40,11 +40,11 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
- | "refreshGlamaModels"
| "refreshOpenRouterModels"
- | "refreshOpenAiModels"
+ | "refreshGlamaModels"
| "refreshUnboundModels"
| "refreshRequestyModels"
+ | "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
@@ -71,7 +71,6 @@ export interface WebviewMessage {
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
- | "refreshGlamaModels"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 1d64f934dc..22564d01a6 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -3674,6 +3674,7 @@
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
@@ -4719,6 +4720,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx
index 4b419957b6..06764a1bfa 100644
--- a/webview-ui/src/components/settings/ApiErrorMessage.tsx
+++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx
@@ -4,13 +4,13 @@ interface ApiErrorMessageProps {
errorMessage: string | undefined
children?: React.ReactNode
}
-const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
- return (
-
-
- {errorMessage}
- {children}
+
+export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => (
+
+
- )
-}
-export default ApiErrorMessage
+ {children}
+
+)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 107f2a483a..c30035cef0 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import * as vscodemodels from "vscode"
-import { Slider } from "@/components/ui"
-
import {
ApiConfiguration,
ModelInfo,
@@ -33,7 +31,6 @@ import {
unboundDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
- THINKING_BUDGET,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
-import ApiErrorMessage from "./ApiErrorMessage"
+import { ApiErrorMessage } from "./ApiErrorMessage"
+import { ThinkingBudget } from "./ThinkingBudget"
+
+const modelsByProvider: Record
> = {
+ anthropic: anthropicModels,
+ bedrock: bedrockModels,
+ vertex: vertexModels,
+ gemini: geminiModels,
+ "openai-native": openAiNativeModels,
+ deepseek: deepSeekModels,
+ mistral: mistralModels,
+}
interface ApiOptionsProps {
uriScheme: string | undefined
@@ -66,18 +74,23 @@ const ApiOptions = ({
const [ollamaModels, setOllamaModels] = useState([])
const [lmStudioModels, setLmStudioModels] = useState([])
const [vsCodeLmModels, setVsCodeLmModels] = useState([])
+
const [openRouterModels, setOpenRouterModels] = useState>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
+
const [glamaModels, setGlamaModels] = useState>({
[glamaDefaultModelId]: glamaDefaultModelInfo,
})
+
const [unboundModels, setUnboundModels] = useState>({
[unboundDefaultModelId]: unboundDefaultModelInfo,
})
+
const [requestyModels, setRequestyModels] = useState>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
+
const [openAiModels, setOpenAiModels] = useState | null>(null)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
@@ -85,8 +98,6 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
-
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
const dropdownEventTransform = (event: DropdownOption | string | undefined) =>
@@ -103,62 +114,87 @@ const ApiOptions = ({
[setApiConfigurationField],
)
- const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
- return normalizeApiConfiguration(apiConfiguration)
- }, [apiConfiguration])
+ const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(
+ () => normalizeApiConfiguration(apiConfiguration),
+ [apiConfiguration],
+ )
- // Pull ollama/lmstudio models
- // Debounced model updates, only executed 250ms after the user stops typing
+ // Debounced refresh model updates, only executed 250ms after the user
+ // stops typing.
useDebounce(
() => {
- if (selectedProvider === "ollama") {
+ if (selectedProvider === "openrouter") {
+ vscode.postMessage({ type: "refreshOpenRouterModels" })
+ } else if (selectedProvider === "glama") {
+ vscode.postMessage({ type: "refreshGlamaModels" })
+ } else if (selectedProvider === "unbound") {
+ vscode.postMessage({ type: "refreshUnboundModels" })
+ } else if (selectedProvider === "requesty") {
+ vscode.postMessage({
+ type: "refreshRequestyModels",
+ values: { apiKey: apiConfiguration?.requestyApiKey },
+ })
+ } else if (selectedProvider === "openai") {
+ vscode.postMessage({
+ type: "refreshOpenAiModels",
+ values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey },
+ })
+ } else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
- } else if (selectedProvider === "openai") {
- vscode.postMessage({
- type: "refreshOpenAiModels",
- values: {
- baseUrl: apiConfiguration?.openAiBaseUrl,
- apiKey: apiConfiguration?.openAiApiKey,
- },
- })
- } else if (selectedProvider === "openrouter") {
- vscode.postMessage({ type: "refreshOpenRouterModels", values: {} })
- } else if (selectedProvider === "glama") {
- vscode.postMessage({ type: "refreshGlamaModels", values: {} })
- } else if (selectedProvider === "requesty") {
- vscode.postMessage({
- type: "refreshRequestyModels",
- values: {
- apiKey: apiConfiguration?.requestyApiKey,
- },
- })
}
},
250,
[
selectedProvider,
- apiConfiguration?.ollamaBaseUrl,
- apiConfiguration?.lmStudioBaseUrl,
+ apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
- apiConfiguration?.requestyApiKey,
+ apiConfiguration?.ollamaBaseUrl,
+ apiConfiguration?.lmStudioBaseUrl,
],
)
useEffect(() => {
const apiValidationResult =
validateApiConfiguration(apiConfiguration) ||
- validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
- setErrorMessage(apiValidationResult)
- }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
+ validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels)
- const handleMessage = useCallback((event: MessageEvent) => {
+ setErrorMessage(apiValidationResult)
+ }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
+
+ const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
+
switch (message.type) {
+ case "openRouterModels": {
+ const updatedModels = message.openRouterModels ?? {}
+ setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "glamaModels": {
+ const updatedModels = message.glamaModels ?? {}
+ setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "unboundModels": {
+ const updatedModels = message.unboundModels ?? {}
+ setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "requestyModels": {
+ const updatedModels = message.requestyModels ?? {}
+ setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "openAiModels": {
+ const updatedModels = message.openAiModels ?? []
+ setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
+ break
+ }
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
@@ -177,72 +213,30 @@ const ApiOptions = ({
setVsCodeLmModels(newModels)
}
break
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
}
}, [])
- useEvent("message", handleMessage)
+ useEvent("message", onMessage)
- const createDropdown = (models: Record) => {
- const options: DropdownOption[] = [
- { value: "", label: "Select a model..." },
- ...Object.keys(models).map((modelId) => ({
- value: modelId,
- label: modelId,
- })),
- ]
-
- return (
- {
- setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
- }}
- style={{ width: "100%" }}
- options={options}
- />
- )
- }
+ const selectedProviderModelOptions: DropdownOption[] = useMemo(
+ () =>
+ modelsByProvider[selectedProvider]
+ ? [
+ { value: "", label: "Select a model..." },
+ ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({
+ value: modelId,
+ label: modelId,
+ })),
+ ]
+ : [],
+ [selectedProvider],
+ )
return (
-
+ {errorMessage &&
}
+
{selectedProvider === "anthropic" && (
- Anthropic API Key
+ Anthropic API Key
- Glama API Key
+ Glama API Key
{!apiConfiguration?.glamaApiKey && (
- Requesty API Key
+ Requesty API Key
- OpenAI API Key
+ OpenAI API Key
- Mistral API Key
+ Mistral API Key
- Codestral Base URL (Optional)
+ Codestral Base URL (Optional)
- OpenRouter API Key
+ OpenRouter API Key
{!apiConfiguration?.openRouterApiKey && (
@@ -530,7 +526,7 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name">
- AWS Profile Name
+ AWS Profile Name
) : (
<>
@@ -541,7 +537,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
- AWS Access Key
+ AWS Access Key
- AWS Secret Key
+ AWS Secret Key
- AWS Session Token
+ AWS Session Token
>
)}
- AWS Region
+ AWS Region
- Google Cloud Project ID
+ Google Cloud Project ID
- Google Cloud Region
+ Google Cloud Region
- {errorMessage && }
- Gemini API Key
+ Gemini API Key
- Base URL
+ Base URL
- API Key
+ API Key
)}
-
-
+
- Max Output Tokens
+ Max Output Tokens
-
Context Window Size
+
Context Window Size
-
Image Support
+
Image Support
- Computer Use
+ Computer Use
-
Input Price
+
Input Price
-
Output Price
+
Output Price
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
- {errorMessage && }
-
{lmStudioModels.length > 0 && (
{" "}
feature to use it with this extension.{" "}
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1154,7 +1141,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
- DeepSeek API Key
+ DeepSeek API Key
- Language Model
+ Language Model
{vsCodeLmModels.length > 0 ? (
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
{errorMessage && (
@@ -1284,7 +1271,7 @@ const ApiOptions = ({
quickstart guide.
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1299,7 +1286,7 @@ const ApiOptions = ({
type="password"
onChange={handleInputChange("unboundApiKey")}
placeholder="Enter API Key...">
- Unbound API Key
+ Unbound API Key
{!apiConfiguration?.unboundApiKey && (
This key is stored locally and only used to make API requests from this extension.
-
)}
- {selectedProvider === "glama" && (
-
- )}
-
{selectedProvider === "openrouter" && (
)}
+
+ {selectedProvider === "glama" && (
+
+ )}
+
+ {selectedProvider === "unbound" && (
+
+ )}
+
{selectedProvider === "requesty" && (
)}
- {selectedProvider !== "glama" &&
- selectedProvider !== "openrouter" &&
- selectedProvider !== "requesty" &&
- selectedProvider !== "openai" &&
- selectedProvider !== "ollama" &&
- selectedProvider !== "lmstudio" &&
- selectedProvider !== "unbound" && (
- <>
-
-
- Model
-
- {selectedProvider === "anthropic" && createDropdown(anthropicModels)}
- {selectedProvider === "bedrock" && createDropdown(bedrockModels)}
- {selectedProvider === "vertex" && createDropdown(vertexModels)}
- {selectedProvider === "gemini" && createDropdown(geminiModels)}
- {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
- {selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
- {selectedProvider === "mistral" && createDropdown(mistralModels)}
-
- {errorMessage && }
- 0 && (
+ <>
+
+
+ Model
+
+
{
+ setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
+ }}
+ options={selectedProviderModelOptions}
+ className="w-full"
/>
- >
- )}
-
- {selectedModelInfo && selectedModelInfo.thinking && (
-
-
Thinking Budget
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
+
+
+ >
)}
{!fromWelcomeView && (
@@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const getProviderData = (models: Record, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
+
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
@@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
+
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
+
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
@@ -1481,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
- case "glama":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
- selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
- }
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@@ -1495,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
+ case "glama":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
+ selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
+ }
+ case "unbound":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
+ selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
+ }
+ case "requesty":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
+ selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
+ }
case "openai":
return {
selectedProvider: provider,
@@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
- supportsImages: false, // VSCode LM API currently doesn't support images
+ supportsImages: false, // VSCode LM API currently doesn't support images.
},
}
- case "unbound":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
- selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
- }
- case "requesty":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
- selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
- }
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index fd62bfb97b..5a7737edd5 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -1,11 +1,13 @@
+import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { useMemo, useState, useCallback, useEffect } from "react"
+
+import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox"
+
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { normalizeApiConfiguration } from "./ApiOptions"
+import { ThinkingBudget } from "./ThinkingBudget"
import { ModelInfoView } from "./ModelInfoView"
-import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
-import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
-import ApiErrorMessage from "./ApiErrorMessage"
type ExtractType = NonNullable<
{ [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration]
@@ -14,24 +16,17 @@ type ExtractType = NonNullable<
type ModelIdKeys = NonNullable<
{ [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
>
-declare module "react" {
- interface CSSProperties {
- // Allow CSS variables
- [key: `--${string}`]: string | number
- }
-}
+
interface ModelPickerProps {
- defaultModelId?: string
+ defaultModelId: string
+ defaultModelInfo?: ModelInfo
models: Record | null
modelIdKey: ModelIdKeys
modelInfoKey: ExtractType
serviceName: string
serviceUrl: string
- recommendedModel: string
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
- defaultModelInfo?: ModelInfo
- errorMessage?: string
}
export const ModelPicker = ({
@@ -41,13 +36,12 @@ export const ModelPicker = ({
modelInfoKey,
serviceName,
serviceUrl,
- recommendedModel,
apiConfiguration,
setApiConfigurationField,
defaultModelInfo,
- errorMessage,
}: ModelPickerProps) => {
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
+ const isInitialized = useRef(false)
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
@@ -55,6 +49,7 @@ export const ModelPicker = ({
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
+
const onSelect = useCallback(
(modelId: string) => {
const modelInfo = models?.[modelId]
@@ -63,26 +58,23 @@ export const ModelPicker = ({
},
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)
+
+ const inputValue = apiConfiguration[modelIdKey]
+
useEffect(() => {
- if (apiConfiguration[modelIdKey] == null && defaultModelId) {
- onSelect(defaultModelId)
+ if (!inputValue && !isInitialized.current) {
+ const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId
+ setApiConfigurationField(modelIdKey, initialValue)
}
- }, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
+
+ isInitialized.current = true
+ }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId])
return (
<>
Model
-
-
+
+
No model found.
{modelIds.map((model) => (
@@ -92,31 +84,18 @@ export const ModelPicker = ({
))}
-
- {errorMessage ? (
-
-
-
- Note: Roo Code uses complex prompts and works best
- with Claude models. Less capable models may not work as expected.
-
-
-
- ) : (
- selectedModelId &&
- selectedModelInfo && (
-
- )
+
+ {selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
+
)}
The extension automatically fetches the latest list of models available on{" "}
@@ -124,7 +103,7 @@ export const ModelPicker = ({
{serviceName}.
If you're unsure which model to choose, Roo Code works best with{" "}
- onSelect(recommendedModel)}>{recommendedModel}.
+ onSelect(defaultModelId)}>{defaultModelId}.
You can also try searching "free" for no-cost options currently available.
>
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index ee032c3ee0..d3e65a99ea 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -66,21 +66,20 @@ const SettingsView = forwardRef(({ onDone },
terminalOutputLineLimit,
writeDelayMs,
} = cachedState
-
+
//Make sure apiConfiguration is initialized and managed by SettingsView
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
- // Update only when currentApiConfigName is changed
- // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration
+ // Update only when currentApiConfigName is changed.
+ // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
if (prevApiConfigName.current === currentApiConfigName) {
return
}
- setCachedState((prevCachedState) => ({
- ...prevCachedState,
- ...extensionState,
- }))
+
+ setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
+ // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
setChangeDetected(false)
}, [currentApiConfigName, extensionState, isChangeDetected])
@@ -90,11 +89,10 @@ const SettingsView = forwardRef(({ onDone },
if (prevState[field] === value) {
return prevState
}
+
+ // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- [field]: value,
- }
+ return { ...prevState, [field]: value }
})
},
[],
@@ -107,15 +105,10 @@ const SettingsView = forwardRef(({ onDone },
return prevState
}
+ // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- apiConfiguration: {
- ...prevState.apiConfiguration,
- [field]: value,
- },
- }
+ return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
})
},
[],
@@ -126,14 +119,19 @@ const SettingsView = forwardRef(({ onDone },
if (prevState.experiments?.[id] === enabled) {
return prevState
}
+
+ // console.log("setExperimentEnabled: setChangeDetected -> true")
setChangeDetected(true)
+
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
})
}, [])
+
const isSettingValid = !errorMessage
+
const handleSubmit = () => {
if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
@@ -160,6 +158,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "updateExperimental", values: experiments })
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
+ // console.log("handleSubmit: setChangeDetected -> false")
setChangeDetected(false)
}
}
@@ -176,13 +175,7 @@ const SettingsView = forwardRef(({ onDone },
[isChangeDetected],
)
- useImperativeHandle(
- ref,
- () => ({
- checkUnsaveChanges,
- }),
- [checkUnsaveChanges],
- )
+ useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
@@ -200,10 +193,7 @@ const SettingsView = forwardRef(({ onDone },
const newCommands = [...currentCommands, commandInput]
setCachedStateField("allowedCommands", newCommands)
setCommandInput("")
- vscode.postMessage({
- type: "allowedCommands",
- commands: newCommands,
- })
+ vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}
}
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
new file mode 100644
index 0000000000..efaa90dc39
--- /dev/null
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -0,0 +1,29 @@
+import { Slider } from "@/components/ui"
+
+import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+
+interface ThinkingBudgetProps {
+ apiConfiguration: ApiConfiguration
+ setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
+ modelInfo?: ModelInfo
+}
+
+export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+ const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
+
+ return modelInfo && modelInfo.thinking ? (
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {budget}
+
+
+ ) : null
+}
diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx
index 7530cae54d..82a25bf8f7 100644
--- a/webview-ui/src/components/ui/alert-dialog.tsx
+++ b/webview-ui/src/components/ui/alert-dialog.tsx
@@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
-const AlertDialog = AlertDialogPrimitive.Root
+function AlertDialog({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+function AlertDialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogPortal = AlertDialogPrimitive.Portal
+function AlertDialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
- ) {
+ return (
+
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+ )
+}
-const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
+function AlertDialogContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
-const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
+function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
+function AlertDialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+function AlertDialogAction({ className, ...props }: React.ComponentProps) {
+ return
+}
+
+function AlertDialogCancel({ className, ...props }: React.ComponentProps) {
+ return
+}
export {
AlertDialog,
diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx
index 11d5e2d3b0..ed3160f692 100644
--- a/webview-ui/src/components/ui/dialog.tsx
+++ b/webview-ui/src/components/ui/dialog.tsx
@@ -1,96 +1,108 @@
-"use client"
-
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { Cross2Icon } from "@radix-ui/react-icons"
+import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
-const Dialog = DialogPrimitive.Root
+function Dialog({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogTrigger = DialogPrimitive.Trigger
+function DialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogPortal = DialogPrimitive.Portal
+function DialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogClose = DialogPrimitive.Close
+function DialogClose({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
-
-const DialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
- ) {
+ return (
+
- {children}
-
-
- Close
-
-
-
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
+ {...props}
+ />
+ )
+}
-const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogHeader.displayName = "DialogHeader"
+function DialogContent({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
-const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogFooter.displayName = "DialogFooter"
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
+function DialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
export {
Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
DialogClose,
DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
}
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 5d880efc0b..ae674c895f 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -12,16 +12,14 @@ const WelcomeView = () => {
const handleSubmit = useCallback(() => {
const error = validateApiConfiguration(apiConfiguration)
+
if (error) {
setErrorMessage(error)
return
}
+
setErrorMessage(undefined)
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentApiConfigName,
- apiConfiguration,
- })
+ vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
}, [apiConfiguration, currentApiConfigName])
return (
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts
index 97c702637c..82af23ab49 100644
--- a/webview-ui/src/utils/validate.ts
+++ b/webview-ui/src/utils/validate.ts
@@ -1,74 +1,83 @@
-import { ApiConfiguration } from "../../../src/shared/api"
-import { ModelInfo } from "../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../src/shared/api"
+
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "anthropic":
- if (!apiConfiguration.apiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "glama":
- if (!apiConfiguration.glamaApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "bedrock":
- if (!apiConfiguration.awsRegion) {
- return "You must choose a region to use with AWS Bedrock."
- }
- break
- case "openrouter":
- if (!apiConfiguration.openRouterApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "vertex":
- if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
- return "You must provide a valid Google Cloud Project ID and Region."
- }
- break
- case "gemini":
- if (!apiConfiguration.geminiApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "openai-native":
- if (!apiConfiguration.openAiNativeApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "mistral":
- if (!apiConfiguration.mistralApiKey) {
- 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."
- }
- break
- case "ollama":
- if (!apiConfiguration.ollamaModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "lmstudio":
- if (!apiConfiguration.lmStudioModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "vscode-lm":
- if (!apiConfiguration.vsCodeLmModelSelector) {
- return "You must provide a valid model selector."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ if (!apiConfiguration.openRouterApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "glama":
+ if (!apiConfiguration.glamaApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "unbound":
+ if (!apiConfiguration.unboundApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "requesty":
+ if (!apiConfiguration.requestyApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "anthropic":
+ if (!apiConfiguration.apiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "bedrock":
+ if (!apiConfiguration.awsRegion) {
+ return "You must choose a region to use with AWS Bedrock."
+ }
+ break
+ case "vertex":
+ if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
+ return "You must provide a valid Google Cloud Project ID and Region."
+ }
+ break
+ case "gemini":
+ if (!apiConfiguration.geminiApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai-native":
+ if (!apiConfiguration.openAiNativeApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "mistral":
+ if (!apiConfiguration.mistralApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai":
+ if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
+ return "You must provide a valid base URL, API key, and model ID."
+ }
+ break
+ case "ollama":
+ if (!apiConfiguration.ollamaModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "lmstudio":
+ if (!apiConfiguration.lmStudioModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "vscode-lm":
+ if (!apiConfiguration.vsCodeLmModelSelector) {
+ return "You must provide a valid model selector."
+ }
+ break
+ }
+
return undefined
}
@@ -77,40 +86,81 @@ export function validateModelId(
glamaModels?: Record,
openRouterModels?: Record,
unboundModels?: Record,
+ requestyModels?: Record,
): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "glama":
- const glamaModelId = apiConfiguration.glamaModelId
- if (!glamaModelId) {
- return "You must provide a model ID."
- }
- if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "openrouter":
- const modelId = apiConfiguration.openRouterModelId
- if (!modelId) {
- return "You must provide a model ID."
- }
- if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "unbound":
- const unboundModelId = apiConfiguration.unboundModelId
- if (!unboundModelId) {
- return "You must provide a model ID."
- }
- if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ const modelId = apiConfiguration.openRouterModelId
+
+ if (!modelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ openRouterModels &&
+ Object.keys(openRouterModels).length > 1 &&
+ !Object.keys(openRouterModels).includes(modelId)
+ ) {
+ return `The model ID (${modelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "glama":
+ const glamaModelId = apiConfiguration.glamaModelId
+
+ if (!glamaModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ glamaModels &&
+ Object.keys(glamaModels).length > 1 &&
+ !Object.keys(glamaModels).includes(glamaModelId)
+ ) {
+ return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "unbound":
+ const unboundModelId = apiConfiguration.unboundModelId
+
+ if (!unboundModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ unboundModels &&
+ Object.keys(unboundModels).length > 1 &&
+ !Object.keys(unboundModels).includes(unboundModelId)
+ ) {
+ return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "requesty":
+ const requestyModelId = apiConfiguration.requestyModelId
+
+ if (!requestyModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ requestyModels &&
+ Object.keys(requestyModels).length > 1 &&
+ !Object.keys(requestyModels).includes(requestyModelId)
+ ) {
+ return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+ }
+
return undefined
}
From 44724e5881ab8daefa7b80bb6c52e5f1bb8a828e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 26 Feb 2025 07:06:25 +0000
Subject: [PATCH 037/298] changeset version bump
---
.changeset/cold-poems-change.md | 5 -----
.changeset/real-ties-destroy.md | 5 -----
.changeset/shaggy-spies-kneel.md | 5 -----
.changeset/swift-kings-attack.md | 5 -----
CHANGELOG.md | 9 +++++++++
package-lock.json | 4 ++--
package.json | 2 +-
7 files changed, 12 insertions(+), 23 deletions(-)
delete mode 100644 .changeset/cold-poems-change.md
delete mode 100644 .changeset/real-ties-destroy.md
delete mode 100644 .changeset/shaggy-spies-kneel.md
delete mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/cold-poems-change.md b/.changeset/cold-poems-change.md
deleted file mode 100644
index 41693ccdfc..0000000000
--- a/.changeset/cold-poems-change.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.5
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
deleted file mode 100644
index a2e9ba8eb0..0000000000
--- a/.changeset/real-ties-destroy.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Fix model picker
diff --git a/.changeset/shaggy-spies-kneel.md b/.changeset/shaggy-spies-kneel.md
deleted file mode 100644
index d137cf85ef..0000000000
--- a/.changeset/shaggy-spies-kneel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add drag-and-drop for files
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
deleted file mode 100644
index 8a8a425611..0000000000
--- a/.changeset/swift-kings-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Pass "thinking" params to OpenRouter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52fb754097..7a9b4b57bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Roo Code Changelog
+## 3.7.5
+
+### Patch Changes
+
+- v3.7.5
+- Fix model picker
+- Add drag-and-drop for files
+- Pass "thinking" params to OpenRouter
+
## [3.7.4]
- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
diff --git a/package-lock.json b/package-lock.json
index 4bcdf8136d..a6c75bd69b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 28045436e6..40bb6a545d 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.4",
+ "version": "3.7.5",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 319f16eb0aabb8033ef94d006b983bdbb45a1eec Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:11:42 -0800
Subject: [PATCH 038/298] Update CHANGELOG
---
CHANGELOG.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a9b4b57bf..ee107be67a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,11 @@
# Roo Code Changelog
-## 3.7.5
+## [3.7.5]
-### Patch Changes
-
-- v3.7.5
-- Fix model picker
+- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
+- Fix various issues with the model picker
- Add drag-and-drop for files
-- Pass "thinking" params to OpenRouter
+- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter
## [3.7.4]
From da1b31765ed05cab6c1d0e25cbae6748aa7ce89a Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:13:52 -0800
Subject: [PATCH 039/298] Update CHANGELOG
---
CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee107be67a..02a4a30cbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,8 @@
## [3.7.5]
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
-- Fix various issues with the model picker
+- Fix various issues with the model picker (thanks @System233!)
+- Fix model input / output cost parsing (thanks @System233!)
- Add drag-and-drop for files
- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter
From 36cfa31d20273880a0e1b84cca85d625d917cce7 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Wed, 26 Feb 2025 12:28:01 +0200
Subject: [PATCH 040/298] try to fix test
---
src/core/webview/__tests__/ClineProvider.test.ts | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index e8b49cc7fc..fe45aaa801 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -860,7 +860,8 @@ describe("ClineProvider", () => {
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
} as unknown as Cline
- provider.addClineToStack(mockCline)
+ const newMockCline = Object.create(mockCline)
+ provider.addClineToStack(newMockCline)
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -872,7 +873,7 @@ describe("ClineProvider", () => {
await messageHandler({ type: "deleteMessage", value: 4000 })
// Verify correct messages were kept
- expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
+ expect(newMockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[4],
@@ -880,7 +881,7 @@ describe("ClineProvider", () => {
])
// Verify correct API messages were kept
- expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
+ expect(newMockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[4],
From 78d5af491aed08505afcfb80bd02a96cb0d287ae Mon Sep 17 00:00:00 2001
From: Joe Manley
Date: Wed, 26 Feb 2025 09:32:13 -0800
Subject: [PATCH 041/298] Fix long strings correctly in ChatRow
---
webview-ui/src/components/chat/ChatRow.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index b139c68f96..4017ccf318 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -617,8 +617,10 @@ export const ChatRowContent = ({
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
- whiteSpace: "pre-line",
- wordWrap: "break-word",
+ overflow: "hidden",
+ whiteSpace: "pre-wrap",
+ wordBreak: "break-word",
+ overflowWrap: "anywhere",
}}>
Date: Wed, 26 Feb 2025 09:41:57 -0800
Subject: [PATCH 042/298] Add changeset
---
.changeset/fluffy-apples-attack.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/fluffy-apples-attack.md
diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md
new file mode 100644
index 0000000000..924a1b2505
--- /dev/null
+++ b/.changeset/fluffy-apples-attack.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Handle really long text in the ChatRow similar to TaskHeader
From d7266be3feb018c5bf207135cb54f74c94d198e5 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 13:52:38 -0500
Subject: [PATCH 043/298] Better OpenRouter error handling
---
.changeset/tender-cycles-help.md | 5 +++++
src/core/Cline.ts | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 .changeset/tender-cycles-help.md
diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md
new file mode 100644
index 0000000000..d43e423ee6
--- /dev/null
+++ b/.changeset/tender-cycles-help.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Better OpenRouter error handling
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 073bd10911..2e29ad453c 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -1010,7 +1010,7 @@ export class Cline {
} catch (error) {
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (alwaysApproveResubmit) {
- const errorMsg = error.message ?? "Unknown error"
+ const errorMsg = error.error?.metadata?.raw ?? error.message ?? "Unknown error"
const baseDelay = requestDelaySeconds || 5
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
// Wait for the greater of the exponential delay or the rate limit delay
From a4e58700ce7527ed486a96df6cce9709fb145074 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 16:04:32 -0500
Subject: [PATCH 044/298] Support multiple files in drag-and-drop
---
.changeset/orange-zoos-train.md | 5 +
.../src/components/chat/ChatTextArea.tsx | 37 ++-
.../chat/__tests__/ChatTextArea.test.tsx | 238 ++++++++++++++++++
3 files changed, 272 insertions(+), 8 deletions(-)
create mode 100644 .changeset/orange-zoos-train.md
diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md
new file mode 100644
index 0000000000..76c16f4567
--- /dev/null
+++ b/.changeset/orange-zoos-train.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Support multiple files in drag-and-drop
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index dc78a3fdb3..be2b2a9798 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -590,15 +590,36 @@ const ChatTextArea = forwardRef(
const files = Array.from(e.dataTransfer.files)
const text = e.dataTransfer.getData("text")
if (text) {
- // Convert the path to a mention-friendly format
- const mentionText = convertToMentionPath(text, cwd)
+ // Split text on newlines to handle multiple files
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
- const newValue =
- inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
- setInputValue(newValue)
- const newCursorPosition = cursorPosition + mentionText.length + 1
- setCursorPosition(newCursorPosition)
- setIntendedCursorPosition(newCursorPosition)
+ if (lines.length > 0) {
+ // Process each line as a separate file path
+ let newValue = inputValue.slice(0, cursorPosition)
+ let totalLength = 0
+
+ lines.forEach((line, index) => {
+ // Convert each path to a mention-friendly format
+ const mentionText = convertToMentionPath(line, cwd)
+ newValue += mentionText
+ totalLength += mentionText.length
+
+ // Add space after each mention except the last one
+ if (index < lines.length - 1) {
+ newValue += " "
+ totalLength += 1
+ }
+ })
+
+ // Add space after the last mention and append the rest of the input
+ newValue += " " + inputValue.slice(cursorPosition)
+ totalLength += 1
+
+ setInputValue(newValue)
+ const newCursorPosition = cursorPosition + totalLength
+ setCursorPosition(newCursorPosition)
+ setIntendedCursorPosition(newCursorPosition)
+ }
return
}
diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
index 205912fc15..3241010e88 100644
--- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
+++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
@@ -3,6 +3,7 @@ import ChatTextArea from "../ChatTextArea"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import { defaultModeSlug } from "../../../../../src/shared/modes"
+import * as pathMentions from "../../../utils/path-mentions"
// Mock modules
jest.mock("../../../utils/vscode", () => ({
@@ -12,9 +13,20 @@ jest.mock("../../../utils/vscode", () => ({
}))
jest.mock("../../../components/common/CodeBlock")
jest.mock("../../../components/common/MarkdownBlock")
+jest.mock("../../../utils/path-mentions", () => ({
+ convertToMentionPath: jest.fn((path, cwd) => {
+ // Simple mock implementation that mimics the real function's behavior
+ if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) {
+ const relativePath = path.substring(cwd.length)
+ return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
+ }
+ return path
+ }),
+}))
// Get the mocked postMessage function
const mockPostMessage = vscode.postMessage as jest.Mock
+const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock
// Mock ExtensionStateContext
jest.mock("../../../context/ExtensionStateContext")
@@ -160,4 +172,230 @@ describe("ChatTextArea", () => {
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
})
})
+
+ describe("multi-file drag and drop", () => {
+ const mockCwd = "/Users/test/project"
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ ;(useExtensionState as jest.Mock).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ cwd: mockCwd,
+ })
+ mockConvertToMentionPath.mockClear()
+ })
+
+ it("should process multiple file paths separated by newlines", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with text data containing multiple file paths
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called for each file path
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd)
+
+ // Verify setInputValue was called with the correct value
+ // The mock implementation of convertToMentionPath will convert the paths to @/file1.js and @/file2.js
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
+ })
+
+ it("should filter out empty lines in the dragged text", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with text data containing empty lines
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n\n/Users/test/project/file2.js\n\n"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called only for non-empty lines
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
+
+ // Verify setInputValue was called with the correct value
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
+ })
+
+ it("should correctly update cursor position after adding multiple mentions", () => {
+ const setInputValue = jest.fn()
+ const initialCursorPosition = 5
+
+ const { container } = render(
+ ,
+ )
+
+ // Set the cursor position manually
+ const textArea = container.querySelector("textarea")
+ if (textArea) {
+ textArea.selectionStart = initialCursorPosition
+ textArea.selectionEnd = initialCursorPosition
+ }
+
+ // Create a mock dataTransfer object with text data
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // The cursor position should be updated based on the implementation in the component
+ expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Hello world")
+ })
+
+ it("should handle very long file paths correctly", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create a very long file path
+ const longPath =
+ "/Users/test/project/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript"
+
+ // Create a mock dataTransfer object with the long path
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(longPath),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called with the long path
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd)
+
+ // The mock implementation will convert it to @/very/long/path/...
+ expect(setInputValue).toHaveBeenCalledWith(
+ "@/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript ",
+ )
+ })
+
+ it("should handle paths with special characters correctly", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create paths with special characters
+ const specialPath1 = "/Users/test/project/file with spaces.js"
+ const specialPath2 = "/Users/test/project/file-with-dashes.js"
+ const specialPath3 = "/Users/test/project/file_with_underscores.js"
+ const specialPath4 = "/Users/test/project/file.with.dots.js"
+
+ // Create a mock dataTransfer object with the special paths
+ const dataTransfer = {
+ getData: jest
+ .fn()
+ .mockReturnValue(`${specialPath1}\n${specialPath2}\n${specialPath3}\n${specialPath4}`),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called for each path
+ expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd)
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd)
+
+ // Verify setInputValue was called with the correct value
+ expect(setInputValue).toHaveBeenCalledWith(
+ "@/file with spaces.js @/file-with-dashes.js @/file_with_underscores.js @/file.with.dots.js ",
+ )
+ })
+
+ it("should handle paths outside the current working directory", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render()
+
+ // Create paths outside the current working directory
+ const outsidePath = "/Users/other/project/file.js"
+
+ // Mock the convertToMentionPath function to return the original path for paths outside cwd
+ mockConvertToMentionPath.mockImplementationOnce((path, cwd) => {
+ return path // Return original path for this test
+ })
+
+ // Create a mock dataTransfer object with the outside path
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(outsidePath),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was called with the outside path
+ expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd)
+
+ // Verify setInputValue was called with the original path
+ expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ")
+ })
+
+ it("should do nothing when dropped text is empty", () => {
+ const setInputValue = jest.fn()
+
+ const { container } = render(
+ ,
+ )
+
+ // Create a mock dataTransfer object with empty text
+ const dataTransfer = {
+ getData: jest.fn().mockReturnValue(""),
+ files: [],
+ }
+
+ // Simulate drop event
+ fireEvent.drop(container.querySelector(".chat-text-area")!, {
+ dataTransfer,
+ preventDefault: jest.fn(),
+ })
+
+ // Verify convertToMentionPath was not called
+ expect(mockConvertToMentionPath).not.toHaveBeenCalled()
+
+ // Verify setInputValue was not called
+ expect(setInputValue).not.toHaveBeenCalled()
+ })
+ })
})
From 5e53d00ebcf0d2adf218a07452d0e15835bf3e64 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Wed, 26 Feb 2025 14:23:11 -0800
Subject: [PATCH 045/298] Allow control over maxTokens for thinking models
---
src/api/providers/anthropic.ts | 12 ++-
src/api/providers/openrouter.ts | 13 ++-
src/core/Cline.ts | 18 +++-
.../__tests__/sliding-window.test.ts | 102 +++++++++++++++---
src/core/sliding-window/index.ts | 37 ++++---
src/core/webview/ClineProvider.ts | 5 +
src/shared/api.ts | 11 +-
src/shared/globalState.ts | 1 +
.../components/settings/ThinkingBudget.tsx | 63 ++++++++---
9 files changed, 197 insertions(+), 65 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index ad58a1cf6b..8c5a1795b1 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -31,7 +31,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, info: modelInfo } = this.getModel()
- const maxTokens = modelInfo.maxTokens || 8192
+ const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens || 8192
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
@@ -41,7 +41,15 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// `claude-3-7-sonnet-20250219` model with a thinking budget.
// We can handle this more elegantly in the future.
modelId = "claude-3-7-sonnet-20250219"
- const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
+
+ // Clamp the thinking budget to be at most 80% of max tokens and at
+ // least 1024 tokens.
+ const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 0a9488e816..69bcb0074c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -108,12 +108,19 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
topP = 0.95
}
+ const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens
let temperature = this.options.modelTemperature ?? defaultTemperature
let thinking: BetaThinkingConfigParam | undefined = undefined
if (modelInfo.thinking) {
- const maxTokens = modelInfo.maxTokens || 8192
- const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
+ // Clamp the thinking budget to be at most 80% of max tokens and at
+ // least 1024 tokens.
+ const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -271,7 +278,7 @@ export async function getOpenRouterModels() {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
- modelInfo.maxTokens = 16384
+ modelInfo.maxTokens = 64_000
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
modelInfo.supportsPromptCache = true
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 073bd10911..fb123e0584 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -87,6 +87,7 @@ export type ClineOptions = {
export class Cline {
readonly taskId: string
+ readonly apiConfiguration: ApiConfiguration
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -148,6 +149,7 @@ export class Cline {
}
this.taskId = crypto.randomUUID()
+ this.apiConfiguration = apiConfiguration
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -961,13 +963,21 @@ export class Cline {
cacheWrites = 0,
cacheReads = 0,
}: ClineApiReqInfo = JSON.parse(previousRequest)
+
const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
- const trimmedMessages = truncateConversationIfNeeded(
- this.apiConversationHistory,
+ const modelInfo = this.api.getModel().info
+ const maxTokens = modelInfo.thinking
+ ? this.apiConfiguration.modelMaxTokens || modelInfo.maxTokens
+ : modelInfo.maxTokens
+ const contextWindow = modelInfo.contextWindow
+
+ const trimmedMessages = truncateConversationIfNeeded({
+ messages: this.apiConversationHistory,
totalTokens,
- this.api.getModel().info,
- )
+ maxTokens,
+ contextWindow,
+ })
if (trimmedMessages !== this.apiConversationHistory) {
await this.overwriteApiConversationHistory(trimmedMessages)
diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts
index 3dcf9e5fd2..cb897aa8cb 100644
--- a/src/core/sliding-window/__tests__/sliding-window.test.ts
+++ b/src/core/sliding-window/__tests__/sliding-window.test.ts
@@ -119,11 +119,21 @@ describe("getMaxTokens", () => {
// Max tokens = 100000 - 50000 = 50000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 49999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 49999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 50001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 50001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -133,11 +143,21 @@ describe("getMaxTokens", () => {
// Max tokens = 100000 - (100000 * 0.2) = 80000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 79999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 79999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 80001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 80001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -147,11 +167,21 @@ describe("getMaxTokens", () => {
// Max tokens = 50000 - 10000 = 40000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 39999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 39999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 40001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 40001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -161,11 +191,21 @@ describe("getMaxTokens", () => {
// Max tokens = 200000 - 30000 = 170000
// Below max tokens - no truncation
- const result1 = truncateConversationIfNeeded(messages, 169999, modelInfo)
+ const result1 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 169999,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result1).toEqual(messages)
// Above max tokens - truncate
- const result2 = truncateConversationIfNeeded(messages, 170001, modelInfo)
+ const result2 = truncateConversationIfNeeded({
+ messages,
+ totalTokens: 170001,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result2).not.toEqual(messages)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -194,7 +234,12 @@ describe("truncateConversationIfNeeded", () => {
const maxTokens = 100000 - 30000 // 70000
const totalTokens = 69999 // Below threshold
- const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+ const result = truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result).toEqual(messages) // No truncation occurs
})
@@ -207,7 +252,12 @@ describe("truncateConversationIfNeeded", () => {
// With 4 messages after the first, 0.5 fraction means remove 2 messages
const expectedResult = [messages[0], messages[3], messages[4]]
- const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo)
+ const result = truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
expect(result).toEqual(expectedResult)
})
@@ -218,14 +268,38 @@ describe("truncateConversationIfNeeded", () => {
// Test below threshold
const belowThreshold = 69999
- expect(truncateConversationIfNeeded(messages, belowThreshold, modelInfo1)).toEqual(
- truncateConversationIfNeeded(messages, belowThreshold, modelInfo2),
+ expect(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: belowThreshold,
+ contextWindow: modelInfo1.contextWindow,
+ maxTokens: modelInfo1.maxTokens,
+ }),
+ ).toEqual(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: belowThreshold,
+ contextWindow: modelInfo2.contextWindow,
+ maxTokens: modelInfo2.maxTokens,
+ }),
)
// Test above threshold
const aboveThreshold = 70001
- expect(truncateConversationIfNeeded(messages, aboveThreshold, modelInfo1)).toEqual(
- truncateConversationIfNeeded(messages, aboveThreshold, modelInfo2),
+ expect(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: aboveThreshold,
+ contextWindow: modelInfo1.contextWindow,
+ maxTokens: modelInfo1.maxTokens,
+ }),
+ ).toEqual(
+ truncateConversationIfNeeded({
+ messages,
+ totalTokens: aboveThreshold,
+ contextWindow: modelInfo2.contextWindow,
+ maxTokens: modelInfo2.maxTokens,
+ }),
)
})
})
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index a0fff05ea5..8b646f933b 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -1,7 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
-import { ModelInfo } from "../../shared/api"
-
/**
* Truncates a conversation by removing a fraction of the messages.
*
@@ -26,28 +24,29 @@ export function truncateConversation(
}
/**
- * Conditionally truncates the conversation messages if the total token count exceeds the model's limit.
+ * Conditionally truncates the conversation messages if the total token count
+ * exceeds the model's limit.
*
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
* @param {number} totalTokens - The total number of tokens in the conversation.
- * @param {ModelInfo} modelInfo - Model metadata including context window size.
+ * @param {number} contextWindow - The context window size.
+ * @param {number} maxTokens - The maximum number of tokens allowed.
* @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
*/
-export function truncateConversationIfNeeded(
- messages: Anthropic.Messages.MessageParam[],
- totalTokens: number,
- modelInfo: ModelInfo,
-): Anthropic.Messages.MessageParam[] {
- return totalTokens < getMaxTokens(modelInfo) ? messages : truncateConversation(messages, 0.5)
+
+type TruncateOptions = {
+ messages: Anthropic.Messages.MessageParam[]
+ totalTokens: number
+ contextWindow: number
+ maxTokens?: number
}
-/**
- * Calculates the maximum allowed tokens
- *
- * @param {ModelInfo} modelInfo - The model information containing the context window size.
- * @returns {number} The maximum number of tokens allowed
- */
-function getMaxTokens(modelInfo: ModelInfo): number {
- // The buffer needs to be at least as large as `modelInfo.maxTokens`, or 20% of the context window if for some reason it's not set.
- return modelInfo.contextWindow - (modelInfo.maxTokens || modelInfo.contextWindow * 0.2)
+export function truncateConversationIfNeeded({
+ messages,
+ totalTokens,
+ contextWindow,
+ maxTokens,
+}: TruncateOptions): Anthropic.Messages.MessageParam[] {
+ const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2)
+ return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index bc6f457868..5e6170e2ee 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1671,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
} = apiConfiguration
await Promise.all([
this.updateGlobalState("apiProvider", apiProvider),
@@ -1719,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelId", requestyModelId),
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
+ this.updateGlobalState("modelMaxTokens", modelMaxTokens),
])
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2210,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
maxOpenTabsContext,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
@@ -2293,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestyModelId") as Promise,
this.getGlobalState("requestyModelInfo") as Promise,
this.getGlobalState("modelTemperature") as Promise,
+ this.getGlobalState("modelMaxTokens") as Promise,
this.getGlobalState("maxOpenTabsContext") as Promise,
])
@@ -2358,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelId,
requestyModelInfo,
modelTemperature,
+ modelMaxTokens,
},
lastShownAnnouncementId,
customInstructions,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 5d4b8b120d..e7e4c54db6 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -68,6 +68,7 @@ export interface ApiHandlerOptions {
requestyModelId?: string
requestyModelInfo?: ModelInfo
modelTemperature?: number
+ modelMaxTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -92,19 +93,13 @@ export interface ModelInfo {
thinking?: boolean
}
-export const THINKING_BUDGET = {
- step: 1024,
- min: 1024,
- default: 8 * 1024,
-}
-
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicModels = {
"claude-3-7-sonnet-20250219:thinking": {
- maxTokens: 16384,
+ maxTokens: 64_000,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
@@ -116,7 +111,7 @@ export const anthropicModels = {
thinking: true,
},
"claude-3-7-sonnet-20250219": {
- maxTokens: 16384,
+ maxTokens: 64_000,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 7b6b4f8274..2cc90456a7 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -81,5 +81,6 @@ export type GlobalStateKey =
| "requestyModelInfo"
| "unboundModelInfo"
| "modelTemperature"
+ | "modelMaxTokens"
| "mistralCodestralUrl"
| "maxOpenTabsContext"
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
index efaa90dc39..5b67874410 100644
--- a/webview-ui/src/components/settings/ThinkingBudget.tsx
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -1,6 +1,8 @@
+import { useEffect } from "react"
+
import { Slider } from "@/components/ui"
-import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
interface ThinkingBudgetProps {
apiConfiguration: ApiConfiguration
@@ -9,21 +11,52 @@ interface ThinkingBudgetProps {
}
export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
- const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
+ const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
+ const tokensMin = 8192
+ const tokensMax = modelInfo?.maxTokens || 64_000
- return modelInfo && modelInfo.thinking ? (
-
-
Thinking Budget
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {budget}
+ const thinkingTokens = apiConfiguration?.anthropicThinking || 8192
+ const thinkingTokensMin = 1024
+ const thinkingTokensMax = Math.floor(0.8 * tokens)
+
+ useEffect(() => {
+ if (thinkingTokens > thinkingTokensMax) {
+ setApiConfigurationField("anthropicThinking", thinkingTokensMax)
+ }
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+
+ if (!modelInfo || !modelInfo.thinking) {
+ return null
+ }
+
+ return (
+
+
+
Max Tokens
+
+
setApiConfigurationField("modelMaxTokens", value)}
+ />
+ {tokens}
+
+
+
+
Max Thinking Tokens
+
+
setApiConfigurationField("anthropicThinking", value)}
+ />
+ {thinkingTokens}
+
- ) : null
+ )
}
From 73c7fe777bb995acd384f719d8ef78d3aa238238 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Thu, 27 Feb 2025 00:24:54 +0200
Subject: [PATCH 046/298] refactored ClineProvider.test
---
.../webview/__tests__/ClineProvider.test.ts | 99 +++++++------------
1 file changed, 35 insertions(+), 64 deletions(-)
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index fe45aaa801..90d2da45f4 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -200,12 +200,15 @@ jest.mock("../../Cline", () => ({
.fn()
.mockImplementation(
(provider, apiConfiguration, customInstructions, diffEnabled, fuzzyMatchThreshold, task, taskId) => ({
+ api: undefined,
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
clineMessages: [],
apiConversationHistory: [],
overwriteClineMessages: jest.fn(),
overwriteApiConversationHistory: jest.fn(),
+ getTaskNumber: jest.fn().mockReturnValue(0),
+ setTaskNumber: jest.fn(),
taskId: taskId || "test-task-id",
}),
),
@@ -407,16 +410,12 @@ describe("ClineProvider", () => {
})
test("clearTask aborts current task", async () => {
- // prepare the mock object
- const mockAbortTask = jest.fn()
- const clineMock = {
- abortTask: mockAbortTask,
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- } as unknown as Cline
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline = new Cline() // Create a new mocked instance
// add the mock object to the stack
- provider.addClineToStack(clineMock)
+ provider.addClineToStack(mockCline)
// get the stack size before the abort call
const stackSizeBeforeAbort = provider.getClineStackSize()
@@ -428,24 +427,19 @@ describe("ClineProvider", () => {
const stackSizeAfterAbort = provider.getClineStackSize()
// check if the abort method was called
- expect(mockAbortTask).toHaveBeenCalled()
+ expect(mockCline.abortTask).toHaveBeenCalled()
// check if the stack size was decreased
expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
})
test("addClineToStack adds multiple Cline instances to the stack", () => {
- // prepare test data
- const mockCline1 = {
- taskId: "test-task-id-1",
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- } as unknown as Cline
- const mockCline2 = {
- taskId: "test-task-id-2",
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- } as unknown as Cline
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline1 = new Cline() // Create a new mocked instance
+ const mockCline2 = new Cline() // Create a new mocked instance
+ Object.defineProperty(mockCline1, "taskId", { value: "test-task-id-1", writable: true })
+ Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true })
// add Cline instances to the stack
provider.addClineToStack(mockCline1)
@@ -848,20 +842,12 @@ describe("ClineProvider", () => {
const mockApiHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }, { ts: 5000 }, { ts: 6000 }]
- // Setup Cline instance with mock data
- const mockCline = {
- clineMessages: mockMessages,
- apiConversationHistory: mockApiHistory,
- overwriteClineMessages: jest.fn(),
- overwriteApiConversationHistory: jest.fn(),
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- taskId: "test-task-id",
- abortTask: jest.fn(),
- handleWebviewAskResponse: jest.fn(),
- } as unknown as Cline
- const newMockCline = Object.create(mockCline)
- provider.addClineToStack(newMockCline)
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline = new Cline() // Create a new mocked instance
+ mockCline.clineMessages = mockMessages // Set test-specific messages
+ mockCline.apiConversationHistory = mockApiHistory // Set API history
+ provider.addClineToStack(mockCline) // Add the mocked instance to the stack
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -873,7 +859,7 @@ describe("ClineProvider", () => {
await messageHandler({ type: "deleteMessage", value: 4000 })
// Verify correct messages were kept
- expect(newMockCline.overwriteClineMessages).toHaveBeenCalledWith([
+ expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[4],
@@ -881,7 +867,7 @@ describe("ClineProvider", () => {
])
// Verify correct API messages were kept
- expect(newMockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
+ expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[4],
@@ -903,18 +889,11 @@ describe("ClineProvider", () => {
const mockApiHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }]
- // Setup Cline instance with mock data
- const mockCline = {
- clineMessages: mockMessages,
- apiConversationHistory: mockApiHistory,
- overwriteClineMessages: jest.fn(),
- overwriteApiConversationHistory: jest.fn(),
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- taskId: "test-task-id",
- abortTask: jest.fn(),
- handleWebviewAskResponse: jest.fn(),
- } as unknown as Cline
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline = new Cline() // Create a new mocked instance
+ mockCline.clineMessages = mockMessages
+ mockCline.apiConversationHistory = mockApiHistory
provider.addClineToStack(mockCline)
// Mock getTaskWithId
@@ -937,15 +916,11 @@ describe("ClineProvider", () => {
// Mock user selecting "Cancel"
;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("Cancel")
- const mockCline = {
- clineMessages: [{ ts: 1000 }, { ts: 2000 }],
- apiConversationHistory: [{ ts: 1000 }, { ts: 2000 }],
- overwriteClineMessages: jest.fn(),
- overwriteApiConversationHistory: jest.fn(),
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- taskId: "test-task-id",
- } as unknown as Cline
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline = new Cline() // Create a new mocked instance
+ mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }]
+ mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }]
provider.addClineToStack(mockCline)
// Trigger message deletion
@@ -1446,13 +1421,9 @@ describe("ClineProvider", () => {
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
} as any
- // Setup mock Cline instance
- const mockCline = {
- api: undefined,
- getTaskNumber: jest.fn(),
- setTaskNumber: jest.fn(),
- abortTask: jest.fn(),
- } as unknown as Cline
+ // Setup Cline instance with auto-mock from the top of the file
+ const { Cline } = require("../../Cline") // Get the mocked class
+ const mockCline = new Cline() // Create a new mocked instance
provider.addClineToStack(mockCline)
const testApiConfig = {
From cf69b0fff92e8b1cff23cf4b37d8e8a2f10a34dc Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Wed, 26 Feb 2025 14:26:27 -0800
Subject: [PATCH 047/298] Add changeset
---
.changeset/wild-emus-dream.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/wild-emus-dream.md
diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md
new file mode 100644
index 0000000000..19e5a4626b
--- /dev/null
+++ b/.changeset/wild-emus-dream.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Allow control over maxTokens for thinking models
From dfa019e7f443bf9997ed2ac2556f597a59e4bf77 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 17:34:39 -0500
Subject: [PATCH 048/298] Truncate results from search_files to 500 chars max
---
.changeset/stale-cooks-help.md | 5 ++
src/services/ripgrep/__tests__/index.test.ts | 51 ++++++++++++++++++++
src/services/ripgrep/index.ts | 30 ++++++++++--
3 files changed, 82 insertions(+), 4 deletions(-)
create mode 100644 .changeset/stale-cooks-help.md
create mode 100644 src/services/ripgrep/__tests__/index.test.ts
diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md
new file mode 100644
index 0000000000..8c9c714738
--- /dev/null
+++ b/.changeset/stale-cooks-help.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Truncate search_file output to avoid crashing the extension
diff --git a/src/services/ripgrep/__tests__/index.test.ts b/src/services/ripgrep/__tests__/index.test.ts
new file mode 100644
index 0000000000..7c3549a827
--- /dev/null
+++ b/src/services/ripgrep/__tests__/index.test.ts
@@ -0,0 +1,51 @@
+// npx jest src/services/ripgrep/__tests__/index.test.ts
+
+import { describe, expect, it } from "@jest/globals"
+import { truncateLine } from "../index"
+
+describe("Ripgrep line truncation", () => {
+ // The default MAX_LINE_LENGTH is 500 in the implementation
+ const MAX_LINE_LENGTH = 500
+
+ it("should truncate lines longer than MAX_LINE_LENGTH", () => {
+ const longLine = "a".repeat(600) // Line longer than MAX_LINE_LENGTH
+ const truncated = truncateLine(longLine)
+
+ expect(truncated).toContain("[truncated...]")
+ expect(truncated.length).toBeLessThan(longLine.length)
+ expect(truncated.length).toEqual(MAX_LINE_LENGTH + " [truncated...]".length)
+ })
+
+ it("should not truncate lines shorter than MAX_LINE_LENGTH", () => {
+ const shortLine = "Short line of text"
+ const truncated = truncateLine(shortLine)
+
+ expect(truncated).toEqual(shortLine)
+ expect(truncated).not.toContain("[truncated...]")
+ })
+
+ it("should correctly truncate a line at exactly MAX_LINE_LENGTH characters", () => {
+ const exactLine = "a".repeat(MAX_LINE_LENGTH)
+ const exactPlusOne = exactLine + "x"
+
+ // Should not truncate when exactly MAX_LINE_LENGTH
+ expect(truncateLine(exactLine)).toEqual(exactLine)
+
+ // Should truncate when exceeding MAX_LINE_LENGTH by even 1 character
+ expect(truncateLine(exactPlusOne)).toContain("[truncated...]")
+ })
+
+ it("should handle empty lines without errors", () => {
+ expect(truncateLine("")).toEqual("")
+ })
+
+ it("should allow custom maximum length", () => {
+ const customLength = 100
+ const line = "a".repeat(customLength + 50)
+
+ const truncated = truncateLine(line, customLength)
+
+ expect(truncated.length).toEqual(customLength + " [truncated...]".length)
+ expect(truncated).toContain("[truncated...]")
+ })
+})
diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts
index b48c60b5b2..770c897e52 100644
--- a/src/services/ripgrep/index.ts
+++ b/src/services/ripgrep/index.ts
@@ -58,7 +58,19 @@ interface SearchResult {
afterContext: string[]
}
+// Constants
const MAX_RESULTS = 300
+const MAX_LINE_LENGTH = 500
+
+/**
+ * Truncates a line if it exceeds the maximum length
+ * @param line The line to truncate
+ * @param maxLength The maximum allowed length (defaults to MAX_LINE_LENGTH)
+ * @returns The truncated line, or the original line if it's shorter than maxLength
+ */
+export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): string {
+ return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line
+}
async function getBinPath(vscodeAppRoot: string): Promise {
const checkPath = async (pkgFolder: string) => {
@@ -140,7 +152,8 @@ export async function regexSearchFiles(
let output: string
try {
output = await execRipgrep(rgPath, args)
- } catch {
+ } catch (error) {
+ console.error("Error executing ripgrep:", error)
return "No results found"
}
const results: SearchResult[] = []
@@ -154,19 +167,28 @@ export async function regexSearchFiles(
if (currentResult) {
results.push(currentResult as SearchResult)
}
+
+ // Safety check: truncate extremely long lines to prevent excessive output
+ const matchText = parsed.data.lines.text
+ const truncatedMatch = truncateLine(matchText)
+
currentResult = {
file: parsed.data.path.text,
line: parsed.data.line_number,
column: parsed.data.submatches[0].start,
- match: parsed.data.lines.text,
+ match: truncatedMatch,
beforeContext: [],
afterContext: [],
}
} else if (parsed.type === "context" && currentResult) {
+ // Apply the same truncation logic to context lines
+ const contextText = parsed.data.lines.text
+ const truncatedContext = truncateLine(contextText)
+
if (parsed.data.line_number < currentResult.line!) {
- currentResult.beforeContext!.push(parsed.data.lines.text)
+ currentResult.beforeContext!.push(truncatedContext)
} else {
- currentResult.afterContext!.push(parsed.data.lines.text)
+ currentResult.afterContext!.push(truncatedContext)
}
}
} catch (error) {
From 247a50a6dcbb29512d20796b29a3240dc4b84463 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 26 Feb 2025 23:17:23 +0000
Subject: [PATCH 049/298] changeset version bump
---
.changeset/fluffy-apples-attack.md | 5 -----
.changeset/orange-zoos-train.md | 5 -----
.changeset/stale-cooks-help.md | 5 -----
.changeset/tender-cycles-help.md | 5 -----
.changeset/wild-emus-dream.md | 5 -----
CHANGELOG.md | 10 ++++++++++
package-lock.json | 4 ++--
package.json | 2 +-
8 files changed, 13 insertions(+), 28 deletions(-)
delete mode 100644 .changeset/fluffy-apples-attack.md
delete mode 100644 .changeset/orange-zoos-train.md
delete mode 100644 .changeset/stale-cooks-help.md
delete mode 100644 .changeset/tender-cycles-help.md
delete mode 100644 .changeset/wild-emus-dream.md
diff --git a/.changeset/fluffy-apples-attack.md b/.changeset/fluffy-apples-attack.md
deleted file mode 100644
index 924a1b2505..0000000000
--- a/.changeset/fluffy-apples-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Handle really long text in the ChatRow similar to TaskHeader
diff --git a/.changeset/orange-zoos-train.md b/.changeset/orange-zoos-train.md
deleted file mode 100644
index 76c16f4567..0000000000
--- a/.changeset/orange-zoos-train.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Support multiple files in drag-and-drop
diff --git a/.changeset/stale-cooks-help.md b/.changeset/stale-cooks-help.md
deleted file mode 100644
index 8c9c714738..0000000000
--- a/.changeset/stale-cooks-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Truncate search_file output to avoid crashing the extension
diff --git a/.changeset/tender-cycles-help.md b/.changeset/tender-cycles-help.md
deleted file mode 100644
index d43e423ee6..0000000000
--- a/.changeset/tender-cycles-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Better OpenRouter error handling
diff --git a/.changeset/wild-emus-dream.md b/.changeset/wild-emus-dream.md
deleted file mode 100644
index 19e5a4626b..0000000000
--- a/.changeset/wild-emus-dream.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Allow control over maxTokens for thinking models
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02a4a30cbd..0e5223231a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Roo Code Changelog
+## 3.7.6
+
+### Patch Changes
+
+- Handle really long text in the ChatRow similar to TaskHeader
+- Support multiple files in drag-and-drop
+- Truncate search_file output to avoid crashing the extension
+- Better OpenRouter error handling
+- Allow control over maxTokens for thinking models
+
## [3.7.5]
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
diff --git a/package-lock.json b/package-lock.json
index a6c75bd69b..808e2f2f10 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.5",
+ "version": "3.7.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.5",
+ "version": "3.7.6",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 40bb6a545d..463e9d597a 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.5",
+ "version": "3.7.6",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 4f578dc8262e03a2a665abcd6784610cc092cdb2 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 18:47:22 -0500
Subject: [PATCH 050/298] Update CHANGELOG.md
---
CHANGELOG.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e5223231a..13b0695335 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,12 @@
# Roo Code Changelog
-## 3.7.6
+## [3.7.6]
-### Patch Changes
-
-- Handle really long text in the ChatRow similar to TaskHeader
+- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
- Support multiple files in drag-and-drop
- Truncate search_file output to avoid crashing the extension
-- Better OpenRouter error handling
-- Allow control over maxTokens for thinking models
+- Better OpenRouter error handling (no more "Provider Error")
+- Add slider to control max output tokens for thinking models
## [3.7.5]
From 5c5bf8502094fb87397eebadda89acd3512dcf84 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 21:36:19 -0500
Subject: [PATCH 051/298] Stop removing commas from terminal output
---
.changeset/sour-parents-hug.md | 5 +++++
src/integrations/terminal/TerminalProcess.ts | 3 ---
2 files changed, 5 insertions(+), 3 deletions(-)
create mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
new file mode 100644
index 0000000000..a24286b6bb
--- /dev/null
+++ b/.changeset/sour-parents-hug.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 5597350db3..4e85c10575 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,9 +110,6 @@ export class TerminalProcess extends EventEmitter {
data = lines.join("\n")
}
- // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
- data = data.replace(/,/g, "")
-
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
From 4806ab5420048af6526348e5b128dd4724c9fcc8 Mon Sep 17 00:00:00 2001
From: dleffel
Date: Wed, 26 Feb 2025 21:34:56 -0800
Subject: [PATCH 052/298] Fix missing tooltips in several components.
---
.../src/components/chat/Announcement.tsx | 1 +
.../src/components/chat/ChatTextArea.tsx | 5 +++
webview-ui/src/components/chat/ChatView.tsx | 33 ++++++++++++++++++-
webview-ui/src/components/chat/TaskHeader.tsx | 13 ++++++--
4 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606ef..93d0c9d750 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -25,6 +25,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index be2b2a9798..dcbe085147 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -798,6 +798,7 @@ const ChatTextArea = forwardRef(
@@ -1101,6 +1102,25 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
+ title={
+ primaryButtonText === "Retry"
+ ? "Try the operation again"
+ : primaryButtonText === "Save"
+ ? "Save the file changes"
+ : primaryButtonText === "Approve"
+ ? "Approve this action"
+ : primaryButtonText === "Run Command"
+ ? "Execute this command"
+ : primaryButtonText === "Start New Task"
+ ? "Begin a new task"
+ : primaryButtonText === "Resume Task"
+ ? "Continue the current task"
+ : primaryButtonText === "Proceed Anyways"
+ ? "Continue despite warnings"
+ : primaryButtonText === "Proceed While Running"
+ ? "Continue while command executes"
+ : undefined
+ }
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
@@ -1113,6 +1133,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
+ title={
+ isStreaming
+ ? "Cancel the current operation"
+ : secondaryButtonText === "Start New Task"
+ ? "Begin a new task"
+ : secondaryButtonText === "Reject"
+ ? "Reject this action"
+ : secondaryButtonText === "Terminate"
+ ? "End the current task"
+ : undefined
+ }
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index 341855f796..fb7db6f617 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -180,7 +180,11 @@ const TaskHeader: React.FC
= ({
${totalCost?.toFixed(4)}
)}
-
+
@@ -348,13 +352,18 @@ export const highlightMentions = (text?: string, withShadow = true) => {
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
-
)
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index d3e65a99ea..51ef4fe81d 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -52,7 +52,7 @@ const SettingsView = forwardRef(({ onDone },
alwaysAllowWrite,
alwaysApproveResubmit,
browserViewportSize,
- checkpointsEnabled,
+ enableCheckpoints,
diffEnabled,
experiments,
fuzzyMatchThreshold,
@@ -143,7 +143,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
- vscode.postMessage({ type: "checkpointsEnabled", bool: checkpointsEnabled })
+ vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -706,6 +706,25 @@ const SettingsView = forwardRef(({ onDone },
+
+
{
+ setCachedStateField("enableCheckpoints", e.target.checked)
+ }}>
+ Enable automatic checkpoints
+
+
+ When enabled, Roo will automatically create checkpoints during task execution, making it
+ easy to review changes or revert to earlier states.
+
+
+
(({ onDone },
)}
-
-
- ⚠️
- {
- setCachedStateField("checkpointsEnabled", e.target.checked)
- }}>
- Enable experimental checkpoints
-
-
-
- When enabled, Roo will save a checkpoint whenever a file in the workspace is modified,
- added or deleted, letting you easily revert to a previous state.
-
-
-
{Object.entries(experimentConfigsMap)
.filter((config) => config[0] !== "DIFF_STRATEGY")
.map((config) => (
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index ae5c5b9539..3dfc87de75 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -32,7 +32,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setSoundEnabled: (value: boolean) => void
setSoundVolume: (value: number) => void
setDiffEnabled: (value: boolean) => void
- setCheckpointsEnabled: (value: boolean) => void
+ setEnableCheckpoints: (value: boolean) => void
setBrowserViewportSize: (value: string) => void
setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string
@@ -79,7 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: true,
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
writeDelayMs: 1000,
@@ -219,7 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
- setCheckpointsEnabled: (value) => setState((prevState) => ({ ...prevState, checkpointsEnabled: value })),
+ setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
setBrowserViewportSize: (value: string) =>
setState((prevState) => ({ ...prevState, browserViewportSize: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
From ea38d9ebbac80f4170f74b4c7d978e588b132d2d Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Thu, 27 Feb 2025 09:04:54 +0100
Subject: [PATCH 054/298] Enable prompt caching for Claude Sonnet 3.7 Vertex AI
model
---
src/shared/api.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 5cda333031..cd6aead1a5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -441,7 +441,7 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
},
From 3bb1d78c17f8caf6d57fb0ebd541c7ea87d9c3e8 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Thu, 27 Feb 2025 18:08:05 +0800
Subject: [PATCH 055/298] feat: Added human relay function and related message
processing initial version
---
.gitignore | 5 +
src/activate/registerCommands.ts | 43 ++++-
src/api/index.ts | 3 +
src/api/providers/human-relay.ts | 162 ++++++++++++++++++
src/core/webview/ClineProvider.ts | 19 ++
src/extension.ts | 36 ++++
src/shared/ExtensionMessage.ts | 21 +++
src/shared/WebviewMessage.ts | 14 ++
src/shared/api.ts | 1 +
webview-ui/src/App.tsx | 52 ++++++
.../human-relay/HumanRelayDialog.tsx | 105 ++++++++++++
.../src/components/settings/ApiOptions.tsx | 25 +++
12 files changed, 483 insertions(+), 3 deletions(-)
create mode 100644 src/api/providers/human-relay.ts
create mode 100644 webview-ui/src/components/human-relay/HumanRelayDialog.tsx
diff --git a/.gitignore b/.gitignore
index 211d06aa19..bdae7b5b26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,8 @@ docs/_site/
#Logging
logs
+.clinerules-architect
+.clinerules-ask
+.clinerules-code
+MemoryBank
+.github/copilot-instructions.md
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 69e257e7a5..8cc895f291 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -3,6 +3,19 @@ import delay from "delay"
import { ClineProvider } from "../core/webview/ClineProvider"
+// Add a global variable to store panel references
+let panel: vscode.WebviewPanel | undefined = undefined
+
+// Get the panel function for command access
+export function getPanel(): vscode.WebviewPanel | undefined {
+ return panel
+}
+
+// Setting the function of the panel
+export function setPanel(newPanel: vscode.WebviewPanel | undefined): void {
+ panel = newPanel
+}
+
export type RegisterCommandOptions = {
context: vscode.ExtensionContext
outputChannel: vscode.OutputChannel
@@ -15,6 +28,22 @@ export const registerCommands = (options: RegisterCommandOptions) => {
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
}
+
+ // Human Relay Dialog Command
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "roo-code.showHumanRelayDialog",
+ (params: { requestId: string; promptText: string }) => {
+ if (getPanel()) {
+ getPanel()?.webview.postMessage({
+ type: "showHumanRelayDialog",
+ requestId: params.requestId,
+ promptText: params.promptText,
+ })
+ }
+ },
+ ),
+ )
}
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
@@ -65,20 +94,28 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit {
+ setPanel(undefined)
+ })
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)
diff --git a/src/api/index.ts b/src/api/index.ts
index f68c9acd1f..85572632ec 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -16,6 +16,7 @@ import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ApiStream } from "./transform/stream"
import { UnboundHandler } from "./providers/unbound"
import { RequestyHandler } from "./providers/requesty"
+import { HumanRelayHandler } from "./providers/human-relay"
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise
@@ -59,6 +60,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new UnboundHandler(options)
case "requesty":
return new RequestyHandler(options)
+ case "human-relay":
+ return new HumanRelayHandler(options)
default:
return new AnthropicHandler(options)
}
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
new file mode 100644
index 0000000000..8454a7c9af
--- /dev/null
+++ b/src/api/providers/human-relay.ts
@@ -0,0 +1,162 @@
+// filepath: e:\Project\Roo-Code\src\api\providers\human-relay.ts
+import { Anthropic } from "@anthropic-ai/sdk"
+import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
+import { ApiHandler, SingleCompletionHandler } from "../index"
+import { ApiStream } from "../transform/stream"
+import * as vscode from "vscode"
+import { ExtensionMessage } from "../../shared/ExtensionMessage"
+
+/**
+ * Human Relay API processor
+ * This processor does not directly call the API, but interacts with the model through human operations copy and paste.
+ */
+export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
+ private options: ApiHandlerOptions
+
+ constructor(options: ApiHandlerOptions) {
+ this.options = options
+ }
+
+ /**
+ * Create a message processing flow, display a dialog box to request human assistance
+ * @param systemPrompt System prompt words
+ * @param messages Message list
+ */
+ async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ // Get the most recent user message
+ const latestMessage = messages[messages.length - 1]
+
+ if (!latestMessage) {
+ throw new Error("No message to relay")
+ }
+
+ // If it is the first message, splice the system prompt word with the user message
+ let promptText = ""
+ if (messages.length === 1) {
+ promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}`
+ } else {
+ promptText = getMessageContent(latestMessage)
+ }
+
+ // Copy to clipboard
+ await vscode.env.clipboard.writeText(promptText)
+
+ // A dialog box pops up to request user action
+ const response = await showHumanRelayDialog(promptText)
+
+ if (!response) {
+ // The user canceled the operation
+ throw new Error("Human relay operation cancelled")
+ }
+
+ // Return to the user input reply
+ yield { type: "text", text: response }
+ }
+
+ /**
+ * Get model information
+ */
+ getModel(): { id: string; info: ModelInfo } {
+ // Human relay does not depend on a specific model, here is a default configuration
+ return {
+ id: "human-relay",
+ info: {
+ maxTokens: 16384,
+ contextWindow: 100000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ supportsComputerUse: true,
+ inputPrice: 0,
+ outputPrice: 0,
+ description: "Calling web-side AI model through human relay",
+ },
+ }
+ }
+
+ /**
+ * Implementation of a single prompt
+ * @param prompt Prompt content
+ */
+ async completePrompt(prompt: string): Promise {
+ // Copy to clipboard
+ await vscode.env.clipboard.writeText(prompt)
+
+ // A dialog box pops up to request user action
+ const response = await showHumanRelayDialog(prompt)
+
+ if (!response) {
+ throw new Error("Human relay operation cancelled")
+ }
+
+ return response
+ }
+}
+
+/**
+ * Extract text content from message object
+ * @param message
+ */
+function getMessageContent(message: Anthropic.Messages.MessageParam): string {
+ if (typeof message.content === "string") {
+ return message.content
+ } else if (Array.isArray(message.content)) {
+ return message.content
+ .filter((item) => item.type === "text")
+ .map((item) => (item.type === "text" ? item.text : ""))
+ .join("\n")
+ }
+ return ""
+}
+/**
+ * Displays the human relay dialog and waits for user response.
+ * @param promptText The prompt text that needs to be copied.
+ * @returns The user's input response or undefined (if canceled).
+ */
+async function showHumanRelayDialog(promptText: string): Promise {
+ return new Promise((resolve) => {
+ // Create a unique request ID
+ const requestId = Date.now().toString()
+
+ // Register callback to the global callback map
+ vscode.commands.executeCommand(
+ "roo-code.registerHumanRelayCallback",
+ requestId,
+ (response: string | undefined) => {
+ resolve(response)
+ },
+ )
+
+ // Show the WebView dialog
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
+
+ // Provide a temporary UI in case the WebView fails to load
+ vscode.window
+ .showInformationMessage(
+ "Please paste the copied message to the AI, then copy the response back into the dialog",
+ {
+ modal: true,
+ detail: "The message has been copied to the clipboard. If the dialog does not open, please try using the input box.",
+ },
+ "Use Input Box",
+ )
+ .then((selection) => {
+ if (selection === "Use Input Box") {
+ // Unregister the callback
+ vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
+
+ vscode.window
+ .showInputBox({
+ prompt: "Please paste the AI's response here",
+ placeHolder: "Paste the AI's response here...",
+ ignoreFocusOut: true,
+ })
+ .then((input) => {
+ resolve(input || undefined)
+ })
+ }
+ })
+ })
+}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5e6170e2ee..1c2ffea550 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1522,7 +1522,26 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Switch back to default mode after deletion
await this.updateGlobalState("mode", defaultModeSlug)
await this.postStateToWebview()
+ break
}
+ case "humanRelayResponse":
+ if (message.requestId && message.text) {
+ vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
+ requestId: message.requestId,
+ text: message.text,
+ cancelled: false,
+ })
+ }
+ break
+
+ case "humanRelayCancel":
+ if (message.requestId) {
+ vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
+ requestId: message.requestId,
+ cancelled: true,
+ })
+ }
+ break
}
},
null,
diff --git a/src/extension.ts b/src/extension.ts
index a05afa4651..3b148a41ac 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -19,6 +19,18 @@ import { McpServerManager } from "./services/mcp/McpServerManager"
let outputChannel: vscode.OutputChannel
let extensionContext: vscode.ExtensionContext
+// Callback mapping of human relay response
+const humanRelayCallbacks = new Map void>()
+
+/**
+ * Register a callback function for human relay response
+ * @param requestId
+ * @param callback
+ */
+export function registerHumanRelayCallback(requestId: string, callback: (response: string | undefined) => void): void {
+ humanRelayCallbacks.set(requestId, callback)
+}
+
// This method is called when your extension is activated.
// Your extension is activated the very first time the command is executed.
export function activate(context: vscode.ExtensionContext) {
@@ -45,6 +57,30 @@ export function activate(context: vscode.ExtensionContext) {
registerCommands({ context, outputChannel, provider: sidebarProvider })
+ // Register human relay response processing command
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "roo-code.handleHumanRelayResponse",
+ (response: { requestId: string; text?: string; cancelled?: boolean }) => {
+ const callback = humanRelayCallbacks.get(response.requestId)
+ if (callback) {
+ if (response.cancelled) {
+ callback(undefined)
+ } else {
+ callback(response.text)
+ }
+ humanRelayCallbacks.delete(response.requestId)
+ }
+ },
+ ),
+ )
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand("roo-code.unregisterHumanRelayCallback", (requestId: string) => {
+ humanRelayCallbacks.delete(requestId)
+ }),
+ )
+
/**
* We use the text document content provider API to show the left side for diff
* view by creating a virtual document for the original content. This makes it
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index e87edffed1..16a18043f5 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -45,6 +45,9 @@ export interface ExtensionMessage {
| "updateCustomMode"
| "deleteCustomMode"
| "currentCheckpointUpdated"
+ | "showHumanRelayDialog"
+ | "humanRelayResponse"
+ | "humanRelayCancel"
text?: string
action?:
| "chatButtonClicked"
@@ -239,4 +242,22 @@ export interface ClineApiReqInfo {
streamingFailedMessage?: string
}
+// Human relay related message types
+export interface ShowHumanRelayDialogMessage {
+ type: "showHumanRelayDialog"
+ requestId: string
+ promptText: string
+}
+
+export interface HumanRelayResponseMessage {
+ type: "humanRelayResponse"
+ requestId: string
+ text: string
+}
+
+export interface HumanRelayCancelMessage {
+ type: "humanRelayCancel"
+ requestId: string
+}
+
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index fde7442cc1..2b0c68f7be 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -94,6 +94,8 @@ export interface WebviewMessage {
| "checkpointRestore"
| "deleteMcpServer"
| "maxOpenTabsContext"
+ | "HumanRelayResponseMessage"
+ | "HumanRelayCancelMessage"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
@@ -119,6 +121,18 @@ export interface WebviewMessage {
source?: "global" | "project"
}
+// Human relay related message types
+export interface HumanRelayResponseMessage {
+ type: "humanRelayResponse"
+ requestId: string
+ text: string
+}
+
+export interface HumanRelayCancelMessage {
+ type: "humanRelayCancel"
+ requestId: string
+}
+
export const checkoutDiffPayloadSchema = z.object({
ts: z.number(),
commitHash: z.string(),
diff --git a/src/shared/api.ts b/src/shared/api.ts
index e7e4c54db6..68b2f87c45 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -16,6 +16,7 @@ export type ApiProvider =
| "mistral"
| "unbound"
| "requesty"
+ | "human-relay"
export interface ApiHandlerOptions {
apiModelId?: string
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 3ae441cd52..5909a3eaef 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -11,6 +11,7 @@ import SettingsView, { SettingsViewRef } from "./components/settings/SettingsVie
import WelcomeView from "./components/welcome/WelcomeView"
import McpView from "./components/mcp/McpView"
import PromptsView from "./components/prompts/PromptsView"
+import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
@@ -28,6 +29,17 @@ const App = () => {
const [tab, setTab] = useState("chat")
const settingsRef = useRef(null)
+ // Human Relay Dialog Status
+ const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
+ isOpen: boolean
+ requestId: string
+ promptText: string
+ }>({
+ isOpen: false,
+ requestId: "",
+ promptText: "",
+ })
+
const switchTab = useCallback((newTab: Tab) => {
if (settingsRef.current?.checkUnsaveChanges) {
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
@@ -47,10 +59,36 @@ const App = () => {
switchTab(newTab)
}
}
+
+ // Processing displays human relay dialog messages
+ if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) {
+ setHumanRelayDialogState({
+ isOpen: true,
+ requestId: message.requestId,
+ promptText: message.promptText,
+ })
+ }
},
[switchTab],
)
+ // Processing Human Relay Dialog Submission
+ const handleHumanRelaySubmit = (requestId: string, text: string) => {
+ vscode.postMessage({
+ type: "humanRelayResponse",
+ requestId,
+ text,
+ })
+ }
+
+ // Handle Human Relay dialog box cancel
+ const handleHumanRelayCancel = (requestId: string) => {
+ vscode.postMessage({
+ type: "humanRelayCancel",
+ requestId,
+ })
+ }
+
useEvent("message", onMessage)
useEffect(() => {
@@ -60,6 +98,11 @@ const App = () => {
}
}, [shouldShowAnnouncement])
+ // Tell Extension that we are ready to receive messages
+ useEffect(() => {
+ vscode.postMessage({ type: "webviewDidLaunch" })
+ }, [])
+
if (!didHydrateState) {
return null
}
@@ -80,6 +123,15 @@ const App = () => {
hideAnnouncement={() => setShowAnnouncement(false)}
showHistoryView={() => switchTab("history")}
/>
+ {/* Human Relay Dialog */}
+ setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
+ onSubmit={handleHumanRelaySubmit}
+ onCancel={handleHumanRelayCancel}
+ />
>
)
}
diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
new file mode 100644
index 0000000000..ea306d11d7
--- /dev/null
+++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
@@ -0,0 +1,105 @@
+import * as React from "react"
+import { Button } from "../ui/button"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"
+import { Textarea } from "../ui/textarea"
+import { useClipboard } from "../ui/hooks"
+import { Check, Copy, X } from "lucide-react"
+
+interface HumanRelayDialogProps {
+ isOpen: boolean
+ onClose: () => void
+ requestId: string
+ promptText: string
+ onSubmit: (requestId: string, text: string) => void
+ onCancel: (requestId: string) => void
+}
+
+/**
+ * Human Relay Dialog Component
+ * Displays the prompt text that needs to be copied and provides an input box for the user to paste the AI's response.
+ */
+export const HumanRelayDialog: React.FC = ({
+ isOpen,
+ onClose,
+ requestId,
+ promptText,
+ onSubmit,
+ onCancel,
+}) => {
+ const [response, setResponse] = React.useState("")
+ const { onCopy } = useClipboard(promptText)
+ const [isCopyClicked, setIsCopyClicked] = React.useState(false)
+
+ // Copy to clipboard and show a success message
+ const handleCopy = () => {
+ onCopy()
+ setIsCopyClicked(true)
+ setTimeout(() => {
+ setIsCopyClicked(false)
+ }, 2000)
+ }
+
+ // Submit the response
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (response.trim()) {
+ onSubmit(requestId, response)
+ onClose()
+ }
+ }
+
+ // Cancel the operation
+ const handleCancel = () => {
+ onCancel(requestId)
+ onClose()
+ }
+
+ return (
+
+ )
+}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c30035cef0..c8598ec101 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -259,6 +259,7 @@ const ApiOptions = ({
{ value: "ollama", label: "Ollama" },
{ value: "unbound", label: "Unbound" },
{ value: "requesty", label: "Requesty" },
+ { value: "human-relay", label: "Human Relay" },
]}
/>
@@ -1307,6 +1308,30 @@ const ApiOptions = ({
)}
+ {selectedProvider === "human-relay" && (
+
+
+ 不需要API key,但需要用户协助复制粘贴信息给web的聊天AI。
+
+
+ 在使用过程中,系统会弹出对话框,并自动复制当前消息到剪贴板。您需要将这些内容粘贴给网页版AI(如ChatGPT或Claude),
+ 然后将AI的回复复制回对话框中点击确认按钮。
+
+
+ )}
+
{selectedProvider === "openrouter" && (
Date: Thu, 27 Feb 2025 02:34:14 -0800
Subject: [PATCH 056/298] Fix AnthropicHandler#completePrompt
---
src/api/providers/anthropic.ts | 98 +++++++++++++++++-----------------
1 file changed, 49 insertions(+), 49 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 8c5a1795b1..eca81eab2e 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -30,29 +30,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- let { id: modelId, info: modelInfo } = this.getModel()
- const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens || 8192
- let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
- let thinking: BetaThinkingConfigParam | undefined = undefined
-
- // Anthropic "Thinking" models require a temperature of 1.0.
- if (modelId === "claude-3-7-sonnet-20250219:thinking") {
- // The `:thinking` variant is a virtual identifier for the
- // `claude-3-7-sonnet-20250219` model with a thinking budget.
- // We can handle this more elegantly in the future.
- modelId = "claude-3-7-sonnet-20250219"
-
- // Clamp the thinking budget to be at most 80% of max tokens and at
- // least 1024 tokens.
- const maxBudgetTokens = Math.floor(maxTokens * 0.8)
- const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
- 1024,
- )
-
- thinking = { type: "enabled", budget_tokens: budgetTokens }
- temperature = 1.0
- }
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
switch (modelId) {
case "claude-3-7-sonnet-20250219":
@@ -202,40 +180,62 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: AnthropicModelId; info: ModelInfo } {
+ getModel() {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
+ let thinking: BetaThinkingConfigParam | undefined = undefined
if (modelId && modelId in anthropicModels) {
- const id = modelId as AnthropicModelId
- return { id, info: anthropicModels[id] }
+ let id = modelId as AnthropicModelId
+ const info: ModelInfo = anthropicModels[id]
+
+ // The `:thinking` variant is a virtual identifier for the
+ // `claude-3-7-sonnet-20250219` model with a thinking budget.
+ // We can handle this more elegantly in the future.
+ if (id === "claude-3-7-sonnet-20250219:thinking") {
+ id = "claude-3-7-sonnet-20250219"
+ }
+
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ if (info.thinking) {
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ temperature = 1.0
+
+ // Clamp the thinking budget to be at most 80% of max tokens and at
+ // least 1024 tokens.
+ const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ 1024,
+ )
+
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
+ }
+
+ return { id, info, temperature, maxTokens, thinking }
}
- return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
+ const id = anthropicDefaultModelId
+ const info: ModelInfo = anthropicModels[id]
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ return { id, info, temperature, maxTokens, thinking }
}
- async completePrompt(prompt: string): Promise {
- try {
- const response = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
- messages: [{ role: "user", content: prompt }],
- stream: false,
- })
+ async completePrompt(prompt: string) {
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
- const content = response.content[0]
+ const message = await this.client.messages.create({
+ model: modelId,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
+ messages: [{ role: "user", content: prompt }],
+ stream: false,
+ })
- if (content.type === "text") {
- return content.text
- }
-
- return ""
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(`Anthropic completion error: ${error.message}`)
- }
-
- throw error
- }
+ const content = message.content.find(({ type }) => type === "text")
+ return content?.type === "text" ? content.text : ""
}
}
From d66b5d2db62f0a6cb8650b8f465d14cf77bbcd36 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 02:40:39 -0800
Subject: [PATCH 057/298] Fix tests
---
src/api/providers/__tests__/anthropic.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/api/providers/__tests__/anthropic.test.ts b/src/api/providers/__tests__/anthropic.test.ts
index ff7bdb4054..82e098f65f 100644
--- a/src/api/providers/__tests__/anthropic.test.ts
+++ b/src/api/providers/__tests__/anthropic.test.ts
@@ -153,7 +153,7 @@ describe("AnthropicHandler", () => {
})
it("should handle API errors", async () => {
- mockCreate.mockRejectedValueOnce(new Error("API Error"))
+ mockCreate.mockRejectedValueOnce(new Error("Anthropic completion error: API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Anthropic completion error: API Error")
})
From 210afc681e799ade14fa2886e07cce0cb6aa2496 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:44:50 -0500
Subject: [PATCH 058/298] v3.7.7
---
.changeset/gorgeous-feet-dress.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
new file mode 100644
index 0000000000..fe2183052d
--- /dev/null
+++ b/.changeset/gorgeous-feet-dress.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+v3.7.7
From dc83617b4d2da06b830e848476a1c5d9179a361a Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 09:51:05 -0500
Subject: [PATCH 059/298] Revert "Stop removing commas from terminal output"
---
.changeset/sour-parents-hug.md | 5 -----
src/integrations/terminal/TerminalProcess.ts | 3 +++
2 files changed, 3 insertions(+), 5 deletions(-)
delete mode 100644 .changeset/sour-parents-hug.md
diff --git a/.changeset/sour-parents-hug.md b/.changeset/sour-parents-hug.md
deleted file mode 100644
index a24286b6bb..0000000000
--- a/.changeset/sour-parents-hug.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Stop removing commas from terminal output
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 4e85c10575..5597350db3 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -110,6 +110,9 @@ export class TerminalProcess extends EventEmitter {
data = lines.join("\n")
}
+ // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
+ data = data.replace(/,/g, "")
+
// 2. Set isHot depending on the command
// Set to hot to stall API requests until terminal is cool again
this.isHot = true
From 8612ab574be39f863329c1b93b32d257bd67450f Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 15:56:03 +0000
Subject: [PATCH 060/298] changeset version bump
---
.changeset/eighty-cheetahs-fetch.md | 5 -----
.changeset/gorgeous-feet-dress.md | 5 -----
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
5 files changed, 10 insertions(+), 13 deletions(-)
delete mode 100644 .changeset/eighty-cheetahs-fetch.md
delete mode 100644 .changeset/gorgeous-feet-dress.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
deleted file mode 100644
index ca103880c8..0000000000
--- a/.changeset/eighty-cheetahs-fetch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Graduate checkpoints out of beta
diff --git a/.changeset/gorgeous-feet-dress.md b/.changeset/gorgeous-feet-dress.md
deleted file mode 100644
index fe2183052d..0000000000
--- a/.changeset/gorgeous-feet-dress.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13b0695335..ff5d8d6aaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Roo Code Changelog
+## 3.7.7
+
+### Patch Changes
+
+- Graduate checkpoints out of beta
+- v3.7.7
+
## [3.7.6]
- Handle really long text better in the in the ChatRow similar to TaskHeader (thanks @joemanley201!)
diff --git a/package-lock.json b/package-lock.json
index 808e2f2f10..c1f748983f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.6",
+ "version": "3.7.7",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 463e9d597a..8441488bac 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.6",
+ "version": "3.7.7",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 4786815fe8bccf47af9202ff8cb99c6f1ef8ad16 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 11:02:13 -0500
Subject: [PATCH 061/298] Update CHANGELOG.md
---
CHANGELOG.md | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff5d8d6aaf..d0cf8f79c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,10 @@
# Roo Code Changelog
-## 3.7.7
-
-### Patch Changes
+## [3.7.7]
- Graduate checkpoints out of beta
-- v3.7.7
+- Fix enhance prompt button when using Thinking Sonnet
+- Add tooltips to make what buttons do more obvious
## [3.7.6]
From eec1769b6b5883d3179e2d1a370ed01b83078286 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 18:56:28 +0000
Subject: [PATCH 062/298] Added cache costs for Claude Sonnet 3.7 via Vertex AI
---
src/shared/api.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/shared/api.ts b/src/shared/api.ts
index e7e4c54db6..d2b4ed728f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -444,6 +444,8 @@ export const vertexModels = {
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
From 1f0211ee6418752201b7d9b34ffb12608ba4a54d Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 23 Feb 2025 20:52:10 -0600
Subject: [PATCH 063/298] Allow users to set custom system prompts
---
src/__mocks__/fs/promises.ts | 1 -
src/__mocks__/jest.setup.ts | 30 +++
.../__tests__/custom-system-prompt.test.ts | 172 ++++++++++++++++++
.../prompts/sections/custom-system-prompt.ts | 60 ++++++
src/core/prompts/system.ts | 15 ++
.../src/components/prompts/PromptsView.tsx | 40 ++++
6 files changed, 317 insertions(+), 1 deletion(-)
create mode 100644 src/core/prompts/__tests__/custom-system-prompt.test.ts
create mode 100644 src/core/prompts/sections/custom-system-prompt.ts
diff --git a/src/__mocks__/fs/promises.ts b/src/__mocks__/fs/promises.ts
index d5f076247a..e496a7fa51 100644
--- a/src/__mocks__/fs/promises.ts
+++ b/src/__mocks__/fs/promises.ts
@@ -140,7 +140,6 @@ const mockFs = {
currentPath += "/" + parts[parts.length - 1]
mockDirectories.add(currentPath)
return Promise.resolve()
- return Promise.resolve()
}),
access: jest.fn().mockImplementation(async (path: string) => {
diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts
index 6bd00e9567..836279bfe4 100644
--- a/src/__mocks__/jest.setup.ts
+++ b/src/__mocks__/jest.setup.ts
@@ -15,3 +15,33 @@ jest.mock("../utils/logging", () => ({
}),
},
}))
+
+// Add toPosix method to String prototype for all tests, mimicking src/utils/path.ts
+// This is needed because the production code expects strings to have this method
+// Note: In production, this is added via import in the entry point (extension.ts)
+export {}
+
+declare global {
+ interface String {
+ toPosix(): string
+ }
+}
+
+// Implementation that matches src/utils/path.ts
+function toPosixPath(p: string) {
+ // Extended-Length Paths in Windows start with "\\?\" to allow longer paths
+ // and bypass usual parsing. If detected, we return the path unmodified.
+ const isExtendedLengthPath = p.startsWith("\\\\?\\")
+
+ if (isExtendedLengthPath) {
+ return p
+ }
+
+ return p.replace(/\\/g, "/")
+}
+
+if (!String.prototype.toPosix) {
+ String.prototype.toPosix = function (this: string): string {
+ return toPosixPath(this)
+ }
+}
diff --git a/src/core/prompts/__tests__/custom-system-prompt.test.ts b/src/core/prompts/__tests__/custom-system-prompt.test.ts
new file mode 100644
index 0000000000..7594c13e6d
--- /dev/null
+++ b/src/core/prompts/__tests__/custom-system-prompt.test.ts
@@ -0,0 +1,172 @@
+import { SYSTEM_PROMPT } from "../system"
+import { defaultModeSlug, modes } from "../../../shared/modes"
+import * as vscode from "vscode"
+import * as fs from "fs/promises"
+
+// Mock the fs/promises module
+jest.mock("fs/promises", () => ({
+ readFile: jest.fn(),
+ mkdir: jest.fn().mockResolvedValue(undefined),
+ access: jest.fn().mockResolvedValue(undefined),
+}))
+
+// Get the mocked fs module
+const mockedFs = fs as jest.Mocked
+
+// Mock the fileExistsAtPath function
+jest.mock("../../../utils/fs", () => ({
+ fileExistsAtPath: jest.fn().mockResolvedValue(true),
+ createDirectoriesForFile: jest.fn().mockResolvedValue([]),
+}))
+
+// Create a mock ExtensionContext with relative paths instead of absolute paths
+const mockContext = {
+ extensionPath: "mock/extension/path",
+ globalStoragePath: "mock/storage/path",
+ storagePath: "mock/storage/path",
+ logPath: "mock/log/path",
+ subscriptions: [],
+ workspaceState: {
+ get: () => undefined,
+ update: () => Promise.resolve(),
+ },
+ globalState: {
+ get: () => undefined,
+ update: () => Promise.resolve(),
+ setKeysForSync: () => {},
+ },
+ extensionUri: { fsPath: "mock/extension/path" },
+ globalStorageUri: { fsPath: "mock/settings/path" },
+ asAbsolutePath: (relativePath: string) => `mock/extension/path/${relativePath}`,
+ extension: {
+ packageJSON: {
+ version: "1.0.0",
+ },
+ },
+} as unknown as vscode.ExtensionContext
+
+describe("File-Based Custom System Prompt", () => {
+ const experiments = {}
+
+ beforeEach(() => {
+ // Reset mocks before each test
+ jest.clearAllMocks()
+
+ // Default behavior: file doesn't exist
+ mockedFs.readFile.mockRejectedValue({ code: "ENOENT" })
+ })
+
+ it("should use default generation when no file-based system prompt is found", async () => {
+ const customModePrompts = {
+ [defaultModeSlug]: {
+ roleDefinition: "Test role definition",
+ },
+ }
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ customModePrompts,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain default sections
+ expect(prompt).toContain("TOOL USE")
+ expect(prompt).toContain("CAPABILITIES")
+ expect(prompt).toContain("MODES")
+ expect(prompt).toContain("Test role definition")
+ })
+
+ it("should use file-based custom system prompt when available", async () => {
+ // Mock the readFile to return content from a file
+ const fileCustomSystemPrompt = "Custom system prompt from file"
+ // When called with utf-8 encoding, return a string
+ mockedFs.readFile.mockImplementation((filePath, options) => {
+ if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+ return Promise.resolve(fileCustomSystemPrompt)
+ }
+ return Promise.reject({ code: "ENOENT" })
+ })
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain role definition and file-based system prompt
+ expect(prompt).toContain(modes[0].roleDefinition)
+ expect(prompt).toContain(fileCustomSystemPrompt)
+
+ // Should not contain any of the default sections
+ expect(prompt).not.toContain("TOOL USE")
+ expect(prompt).not.toContain("CAPABILITIES")
+ expect(prompt).not.toContain("MODES")
+ })
+
+ it("should combine file-based system prompt with role definition and custom instructions", async () => {
+ // Mock the readFile to return content from a file
+ const fileCustomSystemPrompt = "Custom system prompt from file"
+ mockedFs.readFile.mockImplementation((filePath, options) => {
+ if (filePath.toString().includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
+ return Promise.resolve(fileCustomSystemPrompt)
+ }
+ return Promise.reject({ code: "ENOENT" })
+ })
+
+ // Define custom role definition
+ const customRoleDefinition = "Custom role definition"
+ const customModePrompts = {
+ [defaultModeSlug]: {
+ roleDefinition: customRoleDefinition,
+ },
+ }
+
+ const prompt = await SYSTEM_PROMPT(
+ mockContext,
+ "test/path", // Using a relative path without leading slash
+ false,
+ undefined,
+ undefined,
+ undefined,
+ defaultModeSlug,
+ customModePrompts,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ experiments,
+ true,
+ )
+
+ // Should contain custom role definition and file-based system prompt
+ expect(prompt).toContain(customRoleDefinition)
+ expect(prompt).toContain(fileCustomSystemPrompt)
+
+ // Should not contain any of the default sections
+ expect(prompt).not.toContain("TOOL USE")
+ expect(prompt).not.toContain("CAPABILITIES")
+ expect(prompt).not.toContain("MODES")
+ })
+})
diff --git a/src/core/prompts/sections/custom-system-prompt.ts b/src/core/prompts/sections/custom-system-prompt.ts
new file mode 100644
index 0000000000..eca2b98b8d
--- /dev/null
+++ b/src/core/prompts/sections/custom-system-prompt.ts
@@ -0,0 +1,60 @@
+import fs from "fs/promises"
+import path from "path"
+import { Mode } from "../../../shared/modes"
+import { fileExistsAtPath } from "../../../utils/fs"
+
+/**
+ * Safely reads a file, returning an empty string if the file doesn't exist
+ */
+async function safeReadFile(filePath: string): Promise {
+ try {
+ const content = await fs.readFile(filePath, "utf-8")
+ // When reading with "utf-8" encoding, content should be a string
+ return content.trim()
+ } catch (err) {
+ const errorCode = (err as NodeJS.ErrnoException).code
+ if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) {
+ throw err
+ }
+ return ""
+ }
+}
+
+/**
+ * Get the path to a system prompt file for a specific mode
+ */
+export function getSystemPromptFilePath(cwd: string, mode: Mode): string {
+ return path.join(cwd, ".roo", `system-prompt-${mode}`)
+}
+
+/**
+ * Loads custom system prompt from a file at .roo/system-prompt-[mode slug]
+ * If the file doesn't exist, returns an empty string
+ */
+export async function loadSystemPromptFile(cwd: string, mode: Mode): Promise {
+ const filePath = getSystemPromptFilePath(cwd, mode)
+ return safeReadFile(filePath)
+}
+
+/**
+ * Ensures the .roo directory exists, creating it if necessary
+ */
+export async function ensureRooDirectory(cwd: string): Promise {
+ const rooDir = path.join(cwd, ".roo")
+
+ // Check if directory already exists
+ if (await fileExistsAtPath(rooDir)) {
+ return
+ }
+
+ // Create the directory
+ try {
+ await fs.mkdir(rooDir, { recursive: true })
+ } catch (err) {
+ // If directory already exists (race condition), ignore the error
+ const errorCode = (err as NodeJS.ErrnoException).code
+ if (errorCode !== "EEXIST") {
+ throw err
+ }
+ }
+}
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index 91bbd07387..90791f6358 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -23,6 +23,7 @@ import {
getModesSection,
addCustomInstructions,
} from "./sections"
+import { loadSystemPromptFile } from "./sections/custom-system-prompt"
import fs from "fs/promises"
import path from "path"
@@ -119,11 +120,25 @@ export const SYSTEM_PROMPT = async (
return undefined
}
+ // Try to load custom system prompt from file
+ const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode)
+
// Check if it's a custom mode
const promptComponent = getPromptComponent(customModePrompts?.[mode])
+
// Get full mode config from custom modes or fall back to built-in modes
const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
+ // If a file-based custom system prompt exists, use it
+ if (fileCustomSystemPrompt) {
+ const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
+ return `${roleDefinition}
+
+${fileCustomSystemPrompt}
+
+${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}`
+ }
+
// If diff is disabled, don't pass the diffStrategy
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index 061fa789de..2bfafeff5c 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -88,6 +88,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [showConfigMenu, setShowConfigMenu] = useState(false)
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE")
+ const [isSystemPromptDisclosureOpen, setIsSystemPromptDisclosureOpen] = useState(false)
// Direct update functions
const updateAgentPrompt = useCallback(
@@ -971,6 +972,45 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
+ {/* Custom System Prompt Disclosure */}
+
+
setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
+ className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
+ aria-expanded={isSystemPromptDisclosureOpen}>
+
+ Advanced: Override System Prompt
+
+
+ {isSystemPromptDisclosureOpen && (
+
+ You can completely replace the system prompt for this mode (aside from the role
+ definition and custom instructions) by creating a file at{" "}
+ {
+ const currentMode = getCurrentMode()
+ if (!currentMode) return
+
+ // Open or create an empty file
+ vscode.postMessage({
+ type: "openFile",
+ text: `./.roo/system-prompt-${currentMode.slug}`,
+ values: {
+ create: true,
+ content: "",
+ },
+ })
+ }}>
+ .roo/system-prompt-{getCurrentMode()?.slug || "code"}
+ {" "}
+ in your workspace. This is a very advanced feature that bypasses built-in safeguards and
+ consistency checks (especially around tool usage), so be careful!
+
+ )}
+
Date: Thu, 27 Feb 2025 15:18:54 -0500
Subject: [PATCH 064/298] Add gpt-4.5-preview
---
.changeset/flat-avocados-carry.md | 5 +++++
src/api/providers/__tests__/openai-native.test.ts | 2 +-
src/shared/api.ts | 10 +++++++++-
3 files changed, 15 insertions(+), 2 deletions(-)
create mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
new file mode 100644
index 0000000000..f0128f21e0
--- /dev/null
+++ b/.changeset/flat-avocados-carry.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add gpt-4.5-preview
diff --git a/src/api/providers/__tests__/openai-native.test.ts b/src/api/providers/__tests__/openai-native.test.ts
index d6a855849c..eda744c335 100644
--- a/src/api/providers/__tests__/openai-native.test.ts
+++ b/src/api/providers/__tests__/openai-native.test.ts
@@ -357,7 +357,7 @@ describe("OpenAiNativeHandler", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.apiModelId)
expect(modelInfo.info).toBeDefined()
- expect(modelInfo.info.maxTokens).toBe(4096)
+ expect(modelInfo.info.maxTokens).toBe(16384)
expect(modelInfo.info.contextWindow).toBe(128_000)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..47b023ce6f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -678,8 +678,16 @@ export const openAiNativeModels = {
inputPrice: 1.1,
outputPrice: 4.4,
},
+ "gpt-4.5-preview": {
+ maxTokens: 16_384,
+ contextWindow: 128_000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 75,
+ outputPrice: 150,
+ },
"gpt-4o": {
- maxTokens: 4_096,
+ maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
From 820ebc97c5251e04194b014bd60ab51e08de1481 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 27 Feb 2025 20:41:23 +0000
Subject: [PATCH 065/298] changeset version bump
---
.changeset/flat-avocados-carry.md | 5 -----
CHANGELOG.md | 6 ++++++
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/flat-avocados-carry.md
diff --git a/.changeset/flat-avocados-carry.md b/.changeset/flat-avocados-carry.md
deleted file mode 100644
index f0128f21e0..0000000000
--- a/.changeset/flat-avocados-carry.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add gpt-4.5-preview
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0cf8f79c3..e3aa95d448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Roo Code Changelog
+## 3.7.8
+
+### Patch Changes
+
+- Add gpt-4.5-preview
+
## [3.7.7]
- Graduate checkpoints out of beta
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..e7d7718b75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.7",
+ "version": "3.7.8",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 8441488bac..a4a2298a48 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.7",
+ "version": "3.7.8",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From ca7d746990ace8208b5f444ae6f16ea0f25525a1 Mon Sep 17 00:00:00 2001
From: R00-B0T
Date: Thu, 27 Feb 2025 20:41:50 +0000
Subject: [PATCH 066/298] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3aa95d448..fe8156abf7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,6 @@
# Roo Code Changelog
-## 3.7.8
-
-### Patch Changes
+## [3.7.8]
- Add gpt-4.5-preview
From 75e7ef728d0c8512a2f8835a3139bba194f8b327 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 16:09:28 -0500
Subject: [PATCH 067/298] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe8156abf7..9622ce0c99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
## [3.7.8]
+- Add Vertex AI prompt caching support for Claude models (thanks @aitoroses and @lupuletic!)
- Add gpt-4.5-preview
+- Add an advanced feature to customize the system prompt
## [3.7.7]
From c8b78dc7bf29d5dac14974521030cabc47fb5af2 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Thu, 27 Feb 2025 23:33:54 +0200
Subject: [PATCH 068/298] add support ro save paused tasks mode and change back
to it once the task is resumed, also added subtasks logging
---
src/core/Cline.ts | 25 ++++++++++++++++++-
src/core/webview/ClineProvider.ts | 12 ++++++---
.../webview/__tests__/ClineProvider.test.ts | 16 ++++++------
3 files changed, 41 insertions(+), 12 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 397d3aca80..f1b4d2f4d4 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -92,6 +92,7 @@ export class Cline {
private isSubTask: boolean = false
// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
private isPaused: boolean = false
+ private pausedModeSlug: string = defaultModeSlug
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -2642,15 +2643,24 @@ export class Cline {
break
}
+ // before switching roo mode (currently a global settings), save the current mode so we can
+ // resume the parent task (this Cline instance) later with the same mode
+ const currentMode =
+ (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
+ this.pausedModeSlug = currentMode
+
// Switch mode first, then create new task instance
const provider = this.providerRef.deref()
if (provider) {
await provider.handleModeSwitch(mode)
+ this.providerRef
+ .deref()
+ ?.log(`[subtasks] Task: ${this.taskNumber} creating new task in '${mode}' mode`)
await provider.initClineWithSubTask(message)
pushToolResult(
`Successfully created new task in ${targetMode.name} mode with message: ${message}`,
)
- // pasue the current task and start the new task
+ // set the isPaused flag to true so the parent task can wait for the sub-task to finish
this.isPaused = true
} else {
pushToolResult(
@@ -2899,7 +2909,20 @@ export class Cline {
// in this Cline request loop, we need to check if this cline (Task) instance has been asked to wait
// for a sub-task (it has launched) to finish before continuing
if (this.isPaused) {
+ this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has paused`)
await this.waitForResume()
+ this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has resumed`)
+ // waiting for resume is done, resume the task mode
+ const currentMode = (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
+ if (currentMode !== this.pausedModeSlug) {
+ // the mode has changed, we need to switch back to the paused mode
+ await this.providerRef.deref()?.handleModeSwitch(this.pausedModeSlug)
+ this.providerRef
+ .deref()
+ ?.log(
+ `[subtasks] Task: ${this.taskNumber} has switched back to mode: '${this.pausedModeSlug}' from mode: '${currentMode}'`,
+ )
+ }
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 4fa9595ba6..7f412eefde 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -94,7 +94,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Adds a new Cline instance to clineStack, marking the start of a new task.
// The instance is pushed to the top of the stack (LIFO order).
// When the task is completed, the top instance is removed, reactivating the previous task.
- addClineToStack(cline: Cline): void {
+ async addClineToStack(cline: Cline) {
// if cline.getTaskNumber() is -1, it means it is a new task
if (cline.getTaskNumber() === -1) {
// increase last cline number by 1
@@ -107,6 +107,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
// push the cline instance to the stack
this.clineStack.push(cline)
+ // get the current mode
+ const currentMode = (await this.getState()).mode
+ // log the task number and the mode
+ this.log(`[subtasks] Task: ${cline.getTaskNumber()} started at '${currentMode}' mode`)
}
// Removes and destroys the top Cline instance (the current finished task), activating the previous one (resuming the parent task).
@@ -114,9 +118,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// pop the top Cline instance from the stack
var clineToBeRemoved = this.clineStack.pop()
if (clineToBeRemoved) {
+ const removedTaskNumber = clineToBeRemoved.getTaskNumber()
await clineToBeRemoved.abortTask()
// make sure no reference kept, once promises end it will be garbage collected
clineToBeRemoved = undefined
+ this.log(`[subtasks] Task: ${removedTaskNumber} stopped`)
}
// if the stack is empty, reset the last task number
if (this.clineStack.length === 0) {
@@ -417,7 +423,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
images,
experiments,
})
- this.addClineToStack(newCline)
+ await this.addClineToStack(newCline)
}
public async initClineWithHistoryItem(historyItem: HistoryItem) {
@@ -449,7 +455,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
// get this cline task number id from the history item and set it to newCline
newCline.setTaskNumber(historyItem.number)
- this.addClineToStack(newCline)
+ await this.addClineToStack(newCline)
}
public async postMessageToWebview(message: ExtensionMessage) {
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 90d2da45f4..bb646ccef8 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -415,7 +415,7 @@ describe("ClineProvider", () => {
const mockCline = new Cline() // Create a new mocked instance
// add the mock object to the stack
- provider.addClineToStack(mockCline)
+ await provider.addClineToStack(mockCline)
// get the stack size before the abort call
const stackSizeBeforeAbort = provider.getClineStackSize()
@@ -433,7 +433,7 @@ describe("ClineProvider", () => {
expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
})
- test("addClineToStack adds multiple Cline instances to the stack", () => {
+ test("addClineToStack adds multiple Cline instances to the stack", async () => {
// Setup Cline instance with auto-mock from the top of the file
const { Cline } = require("../../Cline") // Get the mocked class
const mockCline1 = new Cline() // Create a new mocked instance
@@ -442,8 +442,8 @@ describe("ClineProvider", () => {
Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true })
// add Cline instances to the stack
- provider.addClineToStack(mockCline1)
- provider.addClineToStack(mockCline2)
+ await provider.addClineToStack(mockCline1)
+ await provider.addClineToStack(mockCline2)
// verify cline instances were added to the stack
expect(provider.getClineStackSize()).toBe(2)
@@ -847,7 +847,7 @@ describe("ClineProvider", () => {
const mockCline = new Cline() // Create a new mocked instance
mockCline.clineMessages = mockMessages // Set test-specific messages
mockCline.apiConversationHistory = mockApiHistory // Set API history
- provider.addClineToStack(mockCline) // Add the mocked instance to the stack
+ await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -894,7 +894,7 @@ describe("ClineProvider", () => {
const mockCline = new Cline() // Create a new mocked instance
mockCline.clineMessages = mockMessages
mockCline.apiConversationHistory = mockApiHistory
- provider.addClineToStack(mockCline)
+ await provider.addClineToStack(mockCline)
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -921,7 +921,7 @@ describe("ClineProvider", () => {
const mockCline = new Cline() // Create a new mocked instance
mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }]
mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }]
- provider.addClineToStack(mockCline)
+ await provider.addClineToStack(mockCline)
// Trigger message deletion
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -1424,7 +1424,7 @@ describe("ClineProvider", () => {
// Setup Cline instance with auto-mock from the top of the file
const { Cline } = require("../../Cline") // Get the mocked class
const mockCline = new Cline() // Create a new mocked instance
- provider.addClineToStack(mockCline)
+ await provider.addClineToStack(mockCline)
const testApiConfig = {
apiProvider: "anthropic" as const,
From 3514f6506b5b0e24919bad29e65b8eba11afced5 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 21:56:56 +0000
Subject: [PATCH 069/298] Added support for Claude Sonnet 3.7 thinking via
Vertex AI
---
package-lock.json | 10 +-
package.json | 2 +-
src/api/providers/vertex.ts | 110 +++++++++++++++---
src/core/webview/ClineProvider.ts | 5 +
src/shared/api.ts | 14 +++
src/shared/globalState.ts | 2 +
.../src/components/settings/ApiOptions.tsx | 3 +
.../components/settings/ThinkingBudget.tsx | 30 +++--
8 files changed, 143 insertions(+), 33 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c1f748983f..547f20a930 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
- "@anthropic-ai/vertex-sdk": "^0.4.1",
+ "@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
@@ -150,11 +150,11 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/@anthropic-ai/vertex-sdk": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.3.tgz",
- "integrity": "sha512-2Uef0C5P2Hx+T88RnUSRA3u4aZqmqnrRSOb2N64ozgKPiSUPTM5JlggAq2b32yWMj5d3MLYa6spJXKMmHXOcoA==",
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.7.0.tgz",
+ "integrity": "sha512-zNm3hUXgYmYDTyveIxOyxbcnh5VXFkrLo4bSnG6LAfGzW7k3k2iCNDSVKtR9qZrK2BCid7JtVu7jsEKaZ/9dSw==",
"dependencies": {
- "@anthropic-ai/sdk": ">=0.14 <1",
+ "@anthropic-ai/sdk": ">=0.35 <1",
"google-auth-library": "^9.4.2"
}
},
diff --git a/package.json b/package.json
index 8441488bac..35db01621a 100644
--- a/package.json
+++ b/package.json
@@ -305,7 +305,7 @@
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
- "@anthropic-ai/vertex-sdk": "^0.4.1",
+ "@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 70562766c3..69fb7d26f7 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { ApiHandler, SingleCompletionHandler } from "../"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
@@ -70,15 +71,25 @@ interface VertexMessageStreamEvent {
usage?: {
output_tokens: number
}
- content_block?: {
- type: "text"
- text: string
- }
+ content_block?:
+ | {
+ type: "text"
+ text: string
+ }
+ | {
+ type: "thinking"
+ thinking: string
+ }
index?: number
- delta?: {
- type: "text_delta"
- text: string
- }
+ delta?:
+ | {
+ type: "text_delta"
+ text: string
+ }
+ | {
+ type: "thinking_delta"
+ thinking: string
+ }
}
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
@@ -145,6 +156,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
+ let { id, info, temperature, maxTokens, thinking } = model
const useCache = model.info.supportsPromptCache
// Find indices of user messages that we want to cache
@@ -158,9 +170,10 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
// Create the stream with appropriate caching configuration
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
// Cache the system prompt if caching is enabled
system: useCache
? [
@@ -220,6 +233,19 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking": {
+ if (chunk.index! > 0) {
+ yield {
+ type: "reasoning",
+ text: "\n",
+ }
+ }
+ yield {
+ type: "reasoning",
+ text: (chunk.content_block as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -232,6 +258,13 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
break
}
+ case "thinking_delta": {
+ yield {
+ type: "reasoning",
+ text: (chunk.delta as any).thinking,
+ }
+ break
+ }
}
break
}
@@ -239,24 +272,63 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
}
- getModel(): { id: VertexModelId; info: ModelInfo } {
+ getModel(): {
+ id: VertexModelId
+ info: ModelInfo
+ temperature: number
+ maxTokens: number
+ thinking?: BetaThinkingConfigParam
+ } {
const modelId = this.options.apiModelId
+ let temperature = this.options.modelTemperature ?? 0
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
if (modelId && modelId in vertexModels) {
const id = modelId as VertexModelId
- return { id, info: vertexModels[id] }
+ const info: ModelInfo = vertexModels[id]
+
+ // The `:thinking` variant is a virtual identifier for thinking-enabled models
+ // Similar to how it's handled in the Anthropic provider
+ let actualId = id
+ if (id.endsWith(":thinking")) {
+ actualId = id.replace(":thinking", "") as VertexModelId
+ }
+
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ if (info.thinking) {
+ temperature = 1.0 // Thinking requires temperature 1.0
+ const maxBudgetTokens = Math.floor(maxTokens * 0.8)
+ const budgetTokens = Math.max(
+ Math.min(
+ this.options.vertexThinking ?? this.options.anthropicThinking ?? maxBudgetTokens,
+ maxBudgetTokens,
+ ),
+ 1024,
+ )
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
+ }
+
+ return { id: actualId, info, temperature, maxTokens, thinking }
}
- return { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
+
+ const id = vertexDefaultModelId
+ const info = vertexModels[id]
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
+
+ return { id, info, temperature, maxTokens, thinking }
}
async completePrompt(prompt: string): Promise {
try {
- const model = this.getModel()
- const useCache = model.info.supportsPromptCache
+ let { id, info, temperature, maxTokens, thinking } = this.getModel()
+ const useCache = info.supportsPromptCache
const params = {
- model: model.id,
- max_tokens: model.info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? 0,
+ model: id,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
system: "", // No system prompt needed for single completions
messages: [
{
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 633c7d7293..5417e54ff7 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1652,6 +1652,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1701,6 +1702,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
this.updateGlobalState("anthropicThinking", anthropicThinking),
+ this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -2158,6 +2160,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2242,6 +2245,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
this.getGlobalState("anthropicThinking") as Promise,
+ this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2343,6 +2347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioBaseUrl,
anthropicBaseUrl,
anthropicThinking,
+ vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 442282d587..f048761d0f 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -41,6 +41,7 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
+ vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -436,6 +437,18 @@ export const openRouterDefaultModelInfo: ModelInfo = {
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
export const vertexModels = {
+ "claude-3-7-sonnet@20250219:thinking": {
+ maxTokens: 64000,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0,
+ outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
+ thinking: true,
+ },
"claude-3-7-sonnet@20250219": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -446,6 +459,7 @@ export const vertexModels = {
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
+ thinking: false,
},
"claude-3-5-sonnet-v2@20241022": {
maxTokens: 8192,
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 0863b34db2..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,6 +24,7 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
+ | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c30035cef0..42ac5cdcb3 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -7,6 +7,7 @@ import * as vscodemodels from "vscode"
import {
ApiConfiguration,
ModelInfo,
+ ApiProvider,
anthropicDefaultModelId,
anthropicModels,
azureOpenAiDefaultApiVersion,
@@ -1380,9 +1381,11 @@ const ApiOptions = ({
/>
(field: K, value: ApiConfiguration[K]) => void
modelInfo?: ModelInfo
+ provider?: ApiProvider
}
-export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+export const ThinkingBudget = ({
+ apiConfiguration,
+ setApiConfigurationField,
+ modelInfo,
+ provider,
+}: ThinkingBudgetProps) => {
+ const isVertexProvider = provider === "vertex"
+ const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
+
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
- const thinkingTokens = apiConfiguration?.anthropicThinking || 8192
+ // Get the appropriate thinking tokens based on provider
+ const thinkingTokens = useMemo(() => {
+ const value = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ return value || Math.min(Math.floor(0.8 * tokens), 8192)
+ }, [apiConfiguration, isVertexProvider, tokens])
+
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField("anthropicThinking", thinkingTokensMax)
+ setApiConfigurationField(budgetField, thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
- if (!modelInfo || !modelInfo.thinking) {
+ if (!modelInfo?.thinking) {
return null
}
@@ -52,7 +66,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField("anthropicThinking", value)}
+ onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
/>
{thinkingTokens}
From 5eba1d53fbeef6f71f027d8317d9f99d120e8026 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:15:17 +0000
Subject: [PATCH 070/298] Added tests for Claude Sonnet Thinking
---
src/api/providers/__tests__/vertex.test.ts | 250 ++++++++++++++++++
.../settings/__tests__/ApiOptions.test.tsx | 57 +++-
.../__tests__/ThinkingBudget.test.tsx | 145 ++++++++++
3 files changed, 451 insertions(+), 1 deletion(-)
create mode 100644 webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 6e81fd771b..076f902ca2 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -2,6 +2,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { VertexHandler } from "../vertex"
import { ApiStreamChunk } from "../../transform/stream"
@@ -431,6 +432,138 @@ describe("VertexHandler", () => {
})
})
+ describe("thinking functionality", () => {
+ const mockMessages: Anthropic.Messages.MessageParam[] = [
+ {
+ role: "user",
+ content: "Hello",
+ },
+ ]
+
+ const systemPrompt = "You are a helpful assistant"
+
+ it("should handle thinking content blocks and deltas", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "thinking",
+ thinking: "Let me think about this...",
+ },
+ },
+ {
+ type: "content_block_delta",
+ delta: {
+ type: "thinking_delta",
+ thinking: " I need to consider all options.",
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 1,
+ content_block: {
+ type: "text",
+ text: "Here's my answer:",
+ },
+ },
+ ]
+
+ // Setup async iterator for mock stream
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ // Verify thinking content is processed correctly
+ const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
+ expect(reasoningChunks).toHaveLength(2)
+ expect(reasoningChunks[0].text).toBe("Let me think about this...")
+ expect(reasoningChunks[1].text).toBe(" I need to consider all options.")
+
+ // Verify text content is processed correctly
+ const textChunks = chunks.filter((chunk) => chunk.type === "text")
+ expect(textChunks).toHaveLength(2) // One for the text block, one for the newline
+ expect(textChunks[0].text).toBe("\n")
+ expect(textChunks[1].text).toBe("Here's my answer:")
+ })
+
+ it("should handle multiple thinking blocks with line breaks", async () => {
+ const mockStream = [
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "thinking",
+ thinking: "First thinking block",
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 1,
+ content_block: {
+ type: "thinking",
+ thinking: "Second thinking block",
+ },
+ },
+ ]
+
+ const asyncIterator = {
+ async *[Symbol.asyncIterator]() {
+ for (const chunk of mockStream) {
+ yield chunk
+ }
+ },
+ }
+
+ const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
+ ;(handler["client"].messages as any).create = mockCreate
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ expect(chunks.length).toBe(3)
+ expect(chunks[0]).toEqual({
+ type: "reasoning",
+ text: "First thinking block",
+ })
+ expect(chunks[1]).toEqual({
+ type: "reasoning",
+ text: "\n",
+ })
+ expect(chunks[2]).toEqual({
+ type: "reasoning",
+ text: "Second thinking block",
+ })
+ })
+ })
+
describe("completePrompt", () => {
it("should complete prompt successfully", async () => {
const result = await handler.completePrompt("Test prompt")
@@ -500,4 +633,121 @@ describe("VertexHandler", () => {
expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model
})
})
+
+ describe("thinking model configuration", () => {
+ it("should configure thinking for models with :thinking suffix", () => {
+ const thinkingHandler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 4096,
+ })
+
+ const modelInfo = thinkingHandler.getModel()
+
+ // Verify thinking configuration
+ expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219")
+ expect(modelInfo.thinking).toBeDefined()
+ const thinkingConfig = modelInfo.thinking as { type: "enabled"; budget_tokens: number }
+ expect(thinkingConfig.type).toBe("enabled")
+ expect(thinkingConfig.budget_tokens).toBe(4096)
+ expect(modelInfo.temperature).toBe(1.0) // Thinking requires temperature 1.0
+ })
+
+ it("should calculate thinking budget correctly", () => {
+ // Test with explicit thinking budget
+ const handlerWithBudget = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 5000,
+ })
+
+ expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
+
+ // Test with default thinking budget (80% of max tokens)
+ const handlerWithDefaultBudget = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 10000,
+ })
+
+ expect((handlerWithDefaultBudget.getModel().thinking as any).budget_tokens).toBe(8000) // 80% of 10000
+
+ // Test with minimum thinking budget (should be at least 1024)
+ const handlerWithSmallMaxTokens = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 1000, // This would result in 800 tokens for thinking, but minimum is 1024
+ })
+
+ expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
+ })
+
+ it("should use anthropicThinking value if vertexThinking is not provided", () => {
+ const handler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ anthropicThinking: 6000, // Should be used as fallback
+ })
+
+ expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
+ })
+
+ it("should pass thinking configuration to API", async () => {
+ const thinkingHandler = new VertexHandler({
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ modelMaxTokens: 16384,
+ vertexThinking: 4096,
+ })
+
+ const mockCreate = jest.fn().mockImplementation(async (options) => {
+ if (!options.stream) {
+ return {
+ id: "test-completion",
+ content: [{ type: "text", text: "Test response" }],
+ role: "assistant",
+ model: options.model,
+ usage: {
+ input_tokens: 10,
+ output_tokens: 5,
+ },
+ }
+ }
+ return {
+ async *[Symbol.asyncIterator]() {
+ yield {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 5,
+ },
+ },
+ }
+ },
+ }
+ })
+ ;(thinkingHandler["client"].messages as any).create = mockCreate
+
+ await thinkingHandler
+ .createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }])
+ .next()
+
+ expect(mockCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ thinking: { type: "enabled", budget_tokens: 4096 },
+ temperature: 1.0, // Thinking requires temperature 1.0
+ }),
+ )
+ })
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 73394bae10..65ae137003 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -46,6 +46,21 @@ jest.mock("../TemperatureControl", () => ({
),
}))
+// Mock ThinkingBudget component
+jest.mock("../ThinkingBudget", () => ({
+ ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
+ modelInfo?.thinking ? (
+
+
+
+ ) : null,
+}))
+
describe("ApiOptions", () => {
const renderApiOptions = (props = {}) => {
render(
@@ -72,5 +87,45 @@ describe("ApiOptions", () => {
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
})
- //TODO: More test cases needed
+ describe("thinking functionality", () => {
+ it("should show ThinkingBudget for Anthropic models that support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "anthropic",
+ apiModelId: "claude-3-7-sonnet-20250219:thinking",
+ },
+ })
+
+ expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+ expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "anthropic")
+ })
+
+ it("should show ThinkingBudget for Vertex models that support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "vertex",
+ apiModelId: "claude-3-7-sonnet@20250219:thinking",
+ },
+ })
+
+ expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
+ expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "vertex")
+ })
+
+ it("should not show ThinkingBudget for models that don't support thinking", () => {
+ renderApiOptions({
+ apiConfiguration: {
+ apiProvider: "anthropic",
+ apiModelId: "claude-3-opus-20240229",
+ modelInfo: { thinking: false }, // Non-thinking model
+ },
+ })
+
+ expect(screen.queryByTestId("thinking-budget")).not.toBeInTheDocument()
+ })
+
+ // Note: We don't need to test the actual ThinkingBudget component functionality here
+ // since we have separate tests for that component. We just need to verify that
+ // it's included in the ApiOptions component when appropriate.
+ })
})
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
new file mode 100644
index 0000000000..54f6b1037b
--- /dev/null
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -0,0 +1,145 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react"
+import { ThinkingBudget } from "../ThinkingBudget"
+import { ApiProvider, ModelInfo } from "../../../../../src/shared/api"
+
+// Mock Slider component
+jest.mock("@/components/ui", () => ({
+ Slider: ({ value, onValueChange, min, max }: any) => (
+ onValueChange([parseInt(e.target.value)])}
+ />
+ ),
+}))
+
+describe("ThinkingBudget", () => {
+ const mockModelInfo: ModelInfo = {
+ thinking: true,
+ maxTokens: 16384,
+ contextWindow: 200000,
+ supportsPromptCache: true,
+ supportsImages: true,
+ }
+ const defaultProps = {
+ apiConfiguration: {},
+ setApiConfigurationField: jest.fn(),
+ modelInfo: mockModelInfo,
+ provider: "anthropic" as ApiProvider,
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("should render nothing when model doesn't support thinking", () => {
+ const { container } = render(
+ ,
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it("should render sliders when model supports thinking", () => {
+ render()
+
+ expect(screen.getAllByTestId("slider")).toHaveLength(2)
+ })
+
+ it("should use anthropicThinking field for Anthropic provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ })
+
+ it("should use vertexThinking field for Vertex provider", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[1], { target: { value: "5000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ })
+
+ it("should cap thinking tokens at 80% of max tokens", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ // Effect should trigger and cap the value
+ expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ })
+
+ it("should use default thinking tokens if not provided", () => {
+ render()
+
+ // Default is 80% of max tokens, capped at 8192
+ const sliders = screen.getAllByTestId("slider")
+ expect(sliders[1]).toHaveValue("8000") // 80% of 10000
+ })
+
+ it("should use min thinking tokens of 1024", () => {
+ render()
+
+ const sliders = screen.getAllByTestId("slider")
+ expect(sliders[1].getAttribute("min")).toBe("1024")
+ })
+
+ it("should update max tokens when slider changes", () => {
+ const setApiConfigurationField = jest.fn()
+
+ render(
+ ,
+ )
+
+ const sliders = screen.getAllByTestId("slider")
+ fireEvent.change(sliders[0], { target: { value: "12000" } })
+
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", 12000)
+ })
+})
From 2b3d23ebd750bfaf19efd6fbcc5bbcc8f1cb3aef Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:17:09 +0000
Subject: [PATCH 071/298] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 87b70cef83bafcf8ea4751de165ba41c35b38ba2 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti
Date: Thu, 27 Feb 2025 22:20:35 +0000
Subject: [PATCH 072/298] Removed unnecessary comment
---
src/shared/globalState.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..05b868a450 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,6 +44,7 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
+ | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From dd4fb6b3097430f98345e85d4c563e29baade089 Mon Sep 17 00:00:00 2001
From: Catalin Lupuleti <105351510+lupuletic@users.noreply.github.com>
Date: Thu, 27 Feb 2025 22:45:12 +0000
Subject: [PATCH 073/298] Update src/shared/globalState.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
---
src/shared/globalState.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 05b868a450..6e29e03835 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -44,7 +44,6 @@ export type GlobalStateKey =
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
- | "vertexThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
From 8cbce2ded08e107454dee7d2eec256973d3e85e0 Mon Sep 17 00:00:00 2001
From: cte
Date: Thu, 27 Feb 2025 16:06:47 -0800
Subject: [PATCH 074/298] Add provider-agnostic modelMaxThinkingTokens setting
---
src/api/providers/__tests__/vertex.test.ts | 18 +++---------------
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 2 +-
src/api/providers/vertex.ts | 5 +----
src/core/webview/ClineProvider.ts | 15 +++++----------
.../__tests__/checkExistApiConfig.test.ts | 2 +-
src/shared/api.ts | 3 +--
src/shared/globalState.ts | 3 +--
.../src/components/settings/ThinkingBudget.tsx | 13 +++++--------
.../settings/__tests__/ApiOptions.test.tsx | 7 +------
.../settings/__tests__/ThinkingBudget.test.tsx | 16 ++++++++--------
11 files changed, 28 insertions(+), 58 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 076f902ca2..9cf92f0a16 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -641,7 +641,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const modelInfo = thinkingHandler.getModel()
@@ -662,7 +662,7 @@ describe("VertexHandler", () => {
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 5000,
+ modelMaxThinkingTokens: 5000,
})
expect((handlerWithBudget.getModel().thinking as any).budget_tokens).toBe(5000)
@@ -688,25 +688,13 @@ describe("VertexHandler", () => {
expect((handlerWithSmallMaxTokens.getModel().thinking as any).budget_tokens).toBe(1024)
})
- it("should use anthropicThinking value if vertexThinking is not provided", () => {
- const handler = new VertexHandler({
- apiModelId: "claude-3-7-sonnet@20250219:thinking",
- vertexProjectId: "test-project",
- vertexRegion: "us-central1",
- modelMaxTokens: 16384,
- anthropicThinking: 6000, // Should be used as fallback
- })
-
- expect((handler.getModel().thinking as any).budget_tokens).toBe(6000)
- })
-
it("should pass thinking configuration to API", async () => {
const thinkingHandler = new VertexHandler({
apiModelId: "claude-3-7-sonnet@20250219:thinking",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
- vertexThinking: 4096,
+ modelMaxThinkingTokens: 4096,
})
const mockCreate = jest.fn().mockImplementation(async (options) => {
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index eca81eab2e..fc0b99c59b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -206,7 +206,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69bcb0074c..82c02e20a7 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -117,7 +117,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// least 1024 tokens.
const maxBudgetTokens = Math.floor((maxTokens || 8192) * 0.8)
const budgetTokens = Math.max(
- Math.min(this.options.anthropicThinking ?? maxBudgetTokens, maxBudgetTokens),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 69fb7d26f7..a25fad07ee 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -300,10 +300,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
temperature = 1.0 // Thinking requires temperature 1.0
const maxBudgetTokens = Math.floor(maxTokens * 0.8)
const budgetTokens = Math.max(
- Math.min(
- this.options.vertexThinking ?? this.options.anthropicThinking ?? maxBudgetTokens,
- maxBudgetTokens,
- ),
+ Math.min(this.options.modelMaxThinkingTokens ?? maxBudgetTokens, maxBudgetTokens),
1024,
)
thinking = { type: "enabled", budget_tokens: budgetTokens }
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5417e54ff7..7b6f2c8971 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1651,8 +1651,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -1673,6 +1671,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
} = apiConfiguration
await Promise.all([
this.updateGlobalState("apiProvider", apiProvider),
@@ -1701,8 +1700,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
- this.updateGlobalState("anthropicThinking", anthropicThinking),
- this.updateGlobalState("vertexThinking", vertexThinking),
this.storeSecret("geminiApiKey", geminiApiKey),
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
@@ -1723,6 +1720,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
this.updateGlobalState("modelMaxTokens", modelMaxTokens),
+ this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
])
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2159,8 +2157,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2216,6 +2212,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
maxOpenTabsContext,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
@@ -2244,8 +2241,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("lmStudioModelId") as Promise,
this.getGlobalState("lmStudioBaseUrl") as Promise,
this.getGlobalState("anthropicBaseUrl") as Promise,
- this.getGlobalState("anthropicThinking") as Promise,
- this.getGlobalState("vertexThinking") as Promise,
this.getSecret("geminiApiKey") as Promise,
this.getSecret("openAiNativeApiKey") as Promise,
this.getSecret("deepSeekApiKey") as Promise,
@@ -2301,6 +2296,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestyModelInfo") as Promise,
this.getGlobalState("modelTemperature") as Promise,
this.getGlobalState("modelMaxTokens") as Promise,
+ this.getGlobalState("anthropicThinking") as Promise,
this.getGlobalState("maxOpenTabsContext") as Promise,
])
@@ -2346,8 +2342,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
- anthropicThinking,
- vertexThinking,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
@@ -2368,6 +2362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestyModelInfo,
modelTemperature,
modelMaxTokens,
+ modelMaxThinkingTokens,
},
lastShownAnnouncementId,
customInstructions,
diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts
index 62517d6958..c99ddddbc4 100644
--- a/src/shared/__tests__/checkExistApiConfig.test.ts
+++ b/src/shared/__tests__/checkExistApiConfig.test.ts
@@ -32,7 +32,7 @@ describe("checkExistKey", () => {
apiKey: "test-key",
apiProvider: undefined,
anthropicBaseUrl: undefined,
- anthropicThinking: undefined,
+ modelMaxThinkingTokens: undefined,
}
expect(checkExistKey(config)).toBe(true)
})
diff --git a/src/shared/api.ts b/src/shared/api.ts
index b36781d630..f88bb5e8b5 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -21,7 +21,6 @@ export interface ApiHandlerOptions {
apiModelId?: string
apiKey?: string // anthropic
anthropicBaseUrl?: string
- anthropicThinking?: number
vsCodeLmModelSelector?: vscode.LanguageModelChatSelector
glamaModelId?: string
glamaModelInfo?: ModelInfo
@@ -41,7 +40,6 @@ export interface ApiHandlerOptions {
awsUseProfile?: boolean
vertexProjectId?: string
vertexRegion?: string
- vertexThinking?: number
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
@@ -70,6 +68,7 @@ export interface ApiHandlerOptions {
requestyModelInfo?: ModelInfo
modelTemperature?: number
modelMaxTokens?: number
+ modelMaxThinkingTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 6e29e03835..aabc77cc01 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -24,7 +24,6 @@ export type GlobalStateKey =
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
- | "vertexThinking"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
@@ -43,7 +42,6 @@ export type GlobalStateKey =
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
- | "anthropicThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
@@ -83,5 +81,6 @@ export type GlobalStateKey =
| "unboundModelInfo"
| "modelTemperature"
| "modelMaxTokens"
+ | "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
| "mistralCodestralUrl"
| "maxOpenTabsContext"
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
index d21e1fb7ea..557a69538d 100644
--- a/webview-ui/src/components/settings/ThinkingBudget.tsx
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -17,27 +17,24 @@ export const ThinkingBudget = ({
modelInfo,
provider,
}: ThinkingBudgetProps) => {
- const isVertexProvider = provider === "vertex"
- const budgetField = isVertexProvider ? "vertexThinking" : "anthropicThinking"
-
const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
// Get the appropriate thinking tokens based on provider
const thinkingTokens = useMemo(() => {
- const value = isVertexProvider ? apiConfiguration?.vertexThinking : apiConfiguration?.anthropicThinking
+ const value = apiConfiguration?.modelMaxThinkingTokens
return value || Math.min(Math.floor(0.8 * tokens), 8192)
- }, [apiConfiguration, isVertexProvider, tokens])
+ }, [apiConfiguration, tokens])
const thinkingTokensMin = 1024
const thinkingTokensMax = Math.floor(0.8 * tokens)
useEffect(() => {
if (thinkingTokens > thinkingTokensMax) {
- setApiConfigurationField(budgetField, thinkingTokensMax)
+ setApiConfigurationField("modelMaxThinkingTokens", thinkingTokensMax)
}
- }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField, budgetField])
+ }, [thinkingTokens, thinkingTokensMax, setApiConfigurationField])
if (!modelInfo?.thinking) {
return null
@@ -66,7 +63,7 @@ export const ThinkingBudget = ({
max={thinkingTokensMax}
step={1024}
value={[thinkingTokens]}
- onValueChange={([value]) => setApiConfigurationField(budgetField, value)}
+ onValueChange={([value]) => setApiConfigurationField("modelMaxThinkingTokens", value)}
/>
{thinkingTokens}
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 65ae137003..06ed95585a 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,12 +51,7 @@ jest.mock("../ThinkingBudget", () => ({
ThinkingBudget: ({ apiConfiguration, setApiConfigurationField, modelInfo, provider }: any) =>
modelInfo?.thinking ? (
-
+
) : null,
}))
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
index 54f6b1037b..212316ea9a 100644
--- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -60,13 +60,13 @@ describe("ThinkingBudget", () => {
expect(screen.getAllByTestId("slider")).toHaveLength(2)
})
- it("should use anthropicThinking field for Anthropic provider", () => {
+ it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -75,16 +75,16 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
- it("should use vertexThinking field for Vertex provider", () => {
+ it("should use modelMaxThinkingTokens field for Vertex provider", () => {
const setApiConfigurationField = jest.fn()
render(
,
@@ -93,7 +93,7 @@ describe("ThinkingBudget", () => {
const sliders = screen.getAllByTestId("slider")
fireEvent.change(sliders[1], { target: { value: "5000" } })
- expect(setApiConfigurationField).toHaveBeenCalledWith("vertexThinking", 5000)
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
})
it("should cap thinking tokens at 80% of max tokens", () => {
@@ -102,13 +102,13 @@ describe("ThinkingBudget", () => {
render(
,
)
// Effect should trigger and cap the value
- expect(setApiConfigurationField).toHaveBeenCalledWith("anthropicThinking", 8000) // 80% of 10000
+ expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 8000) // 80% of 10000
})
it("should use default thinking tokens if not provided", () => {
From 5651b35f89d269ef47defbfa27943bf59f7b852a Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Fri, 28 Feb 2025 04:41:19 +0200
Subject: [PATCH 075/298] added logs, error handling, and added console.log to
the provider log method
---
src/core/Cline.ts | 32 ++++---
src/core/webview/ClineProvider.ts | 133 +++++++++++++++++++++---------
2 files changed, 116 insertions(+), 49 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 17c7fd12e2..fe48d8dbe8 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -543,18 +543,28 @@ export class Cline {
// release this Cline instance from paused state
this.isPaused = false
- // This adds the completion message to conversation history
- await this.say("text", `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`)
+ try {
+ // This adds the completion message to conversation history
+ await this.say(
+ "text",
+ `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`,
+ )
- await this.addToApiConversationHistory({
- role: "user",
- content: [
- {
- type: "text",
- text: `[new_task completed] Result: ${lastMessage ?? "Please continue to the next task."}`,
- },
- ],
- })
+ await this.addToApiConversationHistory({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: `[new_task completed] Result: ${lastMessage ?? "Please continue to the next task."}`,
+ },
+ ],
+ })
+ } catch (error) {
+ this.providerRef
+ .deref()
+ ?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
+ throw error
+ }
try {
// Resume parent task
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 19e385d246..049b4b03f7 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -95,50 +95,102 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// The instance is pushed to the top of the stack (LIFO order).
// When the task is completed, the top instance is removed, reactivating the previous task.
async addClineToStack(cline: Cline) {
- // if cline.getTaskNumber() is -1, it means it is a new task
- if (cline.getTaskNumber() === -1) {
- // increase last cline number by 1
- this.lastTaskNumber = this.lastTaskNumber + 1
- cline.setTaskNumber(this.lastTaskNumber)
+ try {
+ if (!cline || !(cline instanceof Cline)) {
+ throw new Error("Error invalid Cline instance provided.")
+ }
+
+ // Ensure lastTaskNumber is a valid number
+ if (typeof this.lastTaskNumber !== "number") {
+ this.lastTaskNumber = -1
+ }
+
+ const taskNumber = cline.getTaskNumber()
+
+ if (taskNumber === -1) {
+ this.lastTaskNumber += 1
+ cline.setTaskNumber(this.lastTaskNumber)
+ } else if (taskNumber > this.lastTaskNumber) {
+ this.lastTaskNumber = taskNumber
+ }
+
+ this.clineStack.push(cline)
+
+ // Ensure getState() resolves correctly
+ const state = await this.getState()
+ if (!state || typeof state.mode !== "string") {
+ throw new Error("Error failed to retrieve current mode from state.")
+ }
+
+ this.log(`[subtasks] Task: ${cline.getTaskNumber()} started at '${state.mode}' mode`)
+ } catch (error) {
+ this.log(`Error in addClineToStack: ${error.message}`)
+ throw error
}
- // if cline.getTaskNumber() > lastTaskNumber, set lastTaskNumber to cline.getTaskNumber()
- else if (cline.getTaskNumber() > this.lastTaskNumber) {
- this.lastTaskNumber = cline.getTaskNumber()
- }
- // push the cline instance to the stack
- this.clineStack.push(cline)
- // get the current mode
- const currentMode = (await this.getState()).mode
- // log the task number and the mode
- this.log(`[subtasks] Task: ${cline.getTaskNumber()} started at '${currentMode}' mode`)
}
// Removes and destroys the top Cline instance (the current finished task), activating the previous one (resuming the parent task).
async removeClineFromStack() {
- // pop the top Cline instance from the stack
- var clineToBeRemoved = this.clineStack.pop()
- if (clineToBeRemoved) {
- const removedTaskNumber = clineToBeRemoved.getTaskNumber()
- await clineToBeRemoved.abortTask()
- // make sure no reference kept, once promises end it will be garbage collected
- clineToBeRemoved = undefined
- this.log(`[subtasks] Task: ${removedTaskNumber} stopped`)
- }
- // if the stack is empty, reset the last task number
- if (this.clineStack.length === 0) {
- this.lastTaskNumber = -1
+ try {
+ if (!Array.isArray(this.clineStack)) {
+ throw new Error("Error clineStack is not an array.")
+ }
+
+ if (this.clineStack.length === 0) {
+ this.log("[subtasks] No active tasks to remove.")
+ } else {
+ // pop the top Cline instance from the stack
+ var clineToBeRemoved = this.clineStack.pop()
+ if (clineToBeRemoved) {
+ const removedTaskNumber = clineToBeRemoved.getTaskNumber()
+
+ try {
+ await clineToBeRemoved.abortTask()
+ } catch (abortError) {
+ this.log(`Error failed aborting task ${removedTaskNumber}: ${abortError.message}`)
+ }
+
+ // make sure no reference kept, once promises end it will be garbage collected
+ clineToBeRemoved = undefined
+ this.log(`[subtasks] Task: ${removedTaskNumber} stopped`)
+ }
+
+ // if the stack is empty, reset the last task number
+ if (this.clineStack.length === 0) {
+ this.lastTaskNumber = -1
+ }
+ }
+ } catch (error) {
+ this.log(`Error in removeClineFromStack: ${error.message}`)
+ throw error
}
}
// remove the cline object with the received clineId, and all the cline objects bove it in the stack
// for each cline object removed, pop it from the stack, abort the task and set it to undefined
async removeClineWithIdFromStack(clineId: string) {
- const index = this.clineStack.findIndex((c) => c.taskId === clineId)
- if (index === -1) {
- return
- }
- for (let i = this.clineStack.length - 1; i >= index; i--) {
- this.removeClineFromStack()
+ try {
+ if (typeof clineId !== "string" || !clineId.trim()) {
+ throw new Error("Error Invalid clineId provided.")
+ }
+
+ const index = this.clineStack.findIndex((c) => c.taskId === clineId)
+
+ if (index === -1) {
+ this.log(`[subtasks] No task found with ID: ${clineId}`)
+ return
+ }
+
+ for (let i = this.clineStack.length - 1; i >= index; i--) {
+ try {
+ await this.removeClineFromStack()
+ } catch (removalError) {
+ this.log(`Error removing task at stack index ${i}: ${removalError.message}`)
+ }
+ }
+ } catch (error) {
+ this.log(`Error in removeClineWithIdFromStack: ${error.message}`)
+ throw error
}
}
@@ -160,10 +212,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// and resume the previous task/cline instance (if it exists)
// this is used when a sub task is finished and the parent task needs to be resumed
async finishSubTask(lastMessage?: string) {
- // remove the last cline instance from the stack (this is the finished sub task)
- await this.removeClineFromStack()
- // resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
- this.getCurrentCline()?.resumePausedTask(lastMessage)
+ try {
+ // remove the last cline instance from the stack (this is the finished sub task)
+ await this.removeClineFromStack()
+ // resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
+ this.getCurrentCline()?.resumePausedTask(lastMessage)
+ } catch (error) {
+ this.log(`Error in finishSubTask: ${error.message}`)
+ throw error
+ }
}
/*
@@ -2031,7 +2088,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// delete checkpoints branch from project git repo
if (enableCheckpoints && baseDir) {
-
const branchSummary = await simpleGit(baseDir)
.branch(["-D", `roo-code-checkpoints-${id}`])
.catch(() => undefined)
@@ -2591,6 +2647,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
public log(message: string) {
this.outputChannel.appendLine(message)
+ console.log(message)
}
// integration tests
From b1dae213c59da619e87a1183f8a70a86e6f441e3 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Fri, 28 Feb 2025 04:54:14 +0200
Subject: [PATCH 076/298] removed cline instance type check to allow mocks
---
src/core/webview/ClineProvider.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 049b4b03f7..5bbd9c2fec 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -96,7 +96,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// When the task is completed, the top instance is removed, reactivating the previous task.
async addClineToStack(cline: Cline) {
try {
- if (!cline || !(cline instanceof Cline)) {
+ if (!cline) {
throw new Error("Error invalid Cline instance provided.")
}
From 38c09d09d6fadd784a56e1e30165f84d03504e66 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 10:54:25 +0800
Subject: [PATCH 077/298] fix: Fix the human relay dialog function and optimize
user interaction experience
---
src/api/providers/human-relay.ts | 33 ++++++++++++++-----
src/core/webview/ClineProvider.ts | 2 +-
src/extension.ts | 10 ++++++
src/shared/WebviewMessage.ts | 9 ++---
.../src/components/settings/ApiOptions.tsx | 8 +++--
5 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 8454a7c9af..85292a27dc 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -5,6 +5,7 @@ import { ApiHandler, SingleCompletionHandler } from "../index"
import { ApiStream } from "../transform/stream"
import * as vscode from "vscode"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
+import { getPanel } from "../../activate/registerCommands" // 导入 getPanel 函数
/**
* Human Relay API processor
@@ -114,10 +115,10 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string {
*/
async function showHumanRelayDialog(promptText: string): Promise {
return new Promise((resolve) => {
- // Create a unique request ID
+ // 创建一个唯一的请求 ID
const requestId = Date.now().toString()
- // Register callback to the global callback map
+ // 注册全局回调函数
vscode.commands.executeCommand(
"roo-code.registerHumanRelayCallback",
requestId,
@@ -126,13 +127,27 @@ async function showHumanRelayDialog(promptText: string): Promise {
+ // 等待面板创建完成后再显示人工中继对话框
+ setTimeout(() => {
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
+ }, 500) // 给面板创建留出一点时间
+ })
+ } else {
+ // 如果 panel 已存在,直接显示对话框
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
+ }
- // Provide a temporary UI in case the WebView fails to load
+ // 提供临时 UI,以防 WebView 加载失败
vscode.window
.showInformationMessage(
"Please paste the copied message to the AI, then copy the response back into the dialog",
@@ -144,7 +159,7 @@ async function showHumanRelayDialog(promptText: string): Promise {
if (selection === "Use Input Box") {
- // Unregister the callback
+ // 注销回调
vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
vscode.window
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 1c2ffea550..e781a36dbe 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -1522,8 +1522,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Switch back to default mode after deletion
await this.updateGlobalState("mode", defaultModeSlug)
await this.postStateToWebview()
- break
}
+ break
case "humanRelayResponse":
if (message.requestId && message.text) {
vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
diff --git a/src/extension.ts b/src/extension.ts
index 3b148a41ac..87dc7405a3 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -57,6 +57,16 @@ export function activate(context: vscode.ExtensionContext) {
registerCommands({ context, outputChannel, provider: sidebarProvider })
+ // Register human relay callback registration command
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ "roo-code.registerHumanRelayCallback",
+ (requestId: string, callback: (response: string | undefined) => void) => {
+ registerHumanRelayCallback(requestId, callback)
+ },
+ ),
+ )
+
// Register human relay response processing command
context.subscriptions.push(
vscode.commands.registerCommand(
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 2b0c68f7be..a45a727253 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -94,8 +94,8 @@ export interface WebviewMessage {
| "checkpointRestore"
| "deleteMcpServer"
| "maxOpenTabsContext"
- | "HumanRelayResponseMessage"
- | "HumanRelayCancelMessage"
+ | "humanRelayResponse"
+ | "humanRelayCancel"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
@@ -119,16 +119,17 @@ export interface WebviewMessage {
timeout?: number
payload?: WebViewMessagePayload
source?: "global" | "project"
+ requestId?: string
}
// Human relay related message types
-export interface HumanRelayResponseMessage {
+export interface HumanRelayResponseMessage extends WebviewMessage {
type: "humanRelayResponse"
requestId: string
text: string
}
-export interface HumanRelayCancelMessage {
+export interface HumanRelayCancelMessage extends WebviewMessage {
type: "humanRelayCancel"
requestId: string
}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c8598ec101..b125014a62 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1317,7 +1317,8 @@ const ApiOptions = ({
color: "var(--vscode-descriptionForeground)",
lineHeight: "1.4",
}}>
- 不需要API key,但需要用户协助复制粘贴信息给web的聊天AI。
+ The API key is not required, but the user needs to help copy and paste the information to the
+ web chat AI.
- 在使用过程中,系统会弹出对话框,并自动复制当前消息到剪贴板。您需要将这些内容粘贴给网页版AI(如ChatGPT或Claude),
- 然后将AI的回复复制回对话框中点击确认按钮。
+ During use, a dialog box will pop up and the current message will be copied to the clipboard
+ automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude),Then
+ copy the AI's reply back to the dialog box and click the confirm button.
)}
From 1ae4eaa80ebc3f8bc91f669acda4c74b5fd1520f Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 11:42:45 +0800
Subject: [PATCH 078/298] fix: Update comments to the human relay
---
src/api/providers/human-relay.ts | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 85292a27dc..2911e24eaf 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -5,7 +5,7 @@ import { ApiHandler, SingleCompletionHandler } from "../index"
import { ApiStream } from "../transform/stream"
import * as vscode from "vscode"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
-import { getPanel } from "../../activate/registerCommands" // 导入 getPanel 函数
+import { getPanel } from "../../activate/registerCommands" // Import the getPanel function
/**
* Human Relay API processor
@@ -115,10 +115,10 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string {
*/
async function showHumanRelayDialog(promptText: string): Promise {
return new Promise((resolve) => {
- // 创建一个唯一的请求 ID
+ // Create a unique request ID
const requestId = Date.now().toString()
- // 注册全局回调函数
+ // Register a global callback function
vscode.commands.executeCommand(
"roo-code.registerHumanRelayCallback",
requestId,
@@ -127,27 +127,27 @@ async function showHumanRelayDialog(promptText: string): Promise {
- // 等待面板创建完成后再显示人工中继对话框
+ // Wait for the panel to be created before showing the human relay dialog
setTimeout(() => {
vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
requestId,
promptText,
})
- }, 500) // 给面板创建留出一点时间
+ }, 500) // Allow some time for the panel to be created
})
} else {
- // 如果 panel 已存在,直接显示对话框
+ // If the panel already exists, directly show the dialog
vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
requestId,
promptText,
})
}
- // 提供临时 UI,以防 WebView 加载失败
+ // Provide a temporary UI in case the WebView fails to load
vscode.window
.showInformationMessage(
"Please paste the copied message to the AI, then copy the response back into the dialog",
@@ -159,7 +159,7 @@ async function showHumanRelayDialog(promptText: string): Promise {
if (selection === "Use Input Box") {
- // 注销回调
+ // Unregister the callback
vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
vscode.window
From 2c813663f2cb744791561ce420e376474f6b29e1 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Fri, 28 Feb 2025 05:43:27 +0200
Subject: [PATCH 079/298] added connection between task to its parent and root
parent + adjusted cline mock in the provider test
---
src/core/Cline.ts | 29 +++++++++++++++++++
src/core/webview/ClineProvider.ts | 7 +++++
.../webview/__tests__/ClineProvider.test.ts | 2 ++
3 files changed, 38 insertions(+)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index fe48d8dbe8..d05f9320d4 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -92,7 +92,12 @@ export class Cline {
private isSubTask: boolean = false
// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
private isPaused: boolean = false
+ // this is the parent task work mode when it launched the subtask to be used when it is restored (so the last used mode by parent task will also be restored)
private pausedModeSlug: string = defaultModeSlug
+ // if this is a subtask then this member holds a pointer to the parent task that launched it
+ private parentTask: Cline | undefined = undefined
+ // if this is a subtask then this member holds a pointer to the top parent task that launched it
+ private rootTask: Cline | undefined = undefined
readonly apiConfiguration: ApiConfiguration
api: ApiHandler
private terminalManager: TerminalManager
@@ -218,6 +223,30 @@ export class Cline {
return this.taskNumber
}
+ // this method returns the cline instance that is the parent task that launched this subtask (assuming this cline is a subtask)
+ // if undefined is returned, then there is no parent task and this is not a subtask or connection has been severed
+ getParentTask(): Cline | undefined {
+ return this.parentTask
+ }
+
+ // this method sets a cline instance that is the parent task that called this task (assuming this cline is a subtask)
+ // if undefined is set, then the connection is broken and the parent is no longer saved in the subtask member
+ setParentTask(parentToSet: Cline | undefined) {
+ this.parentTask = parentToSet
+ }
+
+ // this method returns the cline instance that is the root task (top most parent) that eventually launched this subtask (assuming this cline is a subtask)
+ // if undefined is returned, then there is no root task and this is not a subtask or connection has been severed
+ getRootTask(): Cline | undefined {
+ return this.rootTask
+ }
+
+ // this method sets a cline instance that is the root task (top most patrnt) that called this task (assuming this cline is a subtask)
+ // if undefined is set, then the connection is broken and the root is no longer saved in the subtask member
+ setRootTask(rootToSet: Cline | undefined) {
+ this.rootTask = rootToSet
+ }
+
// Add method to update diffStrategy
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
// If not provided, get from current state
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5bbd9c2fec..4249081277 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -114,6 +114,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.lastTaskNumber = taskNumber
}
+ // set this cline task parent cline (the task that launched it), and the root cline (the top most task that eventually launched it)
+ if (this.clineStack.length >= 1) {
+ cline.setParentTask(this.getCurrentCline())
+ cline.setRootTask(this.clineStack[0])
+ }
+
+ // add this cline instance into the stack that represents the order of all the called tasks
this.clineStack.push(cline)
// Ensure getState() resolves correctly
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 987baeefaf..7700ae890e 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -209,6 +209,8 @@ jest.mock("../../Cline", () => ({
overwriteApiConversationHistory: jest.fn(),
getTaskNumber: jest.fn().mockReturnValue(0),
setTaskNumber: jest.fn(),
+ setParentTask: jest.fn(),
+ setRootTask: jest.fn(),
taskId: taskId || "test-task-id",
}),
),
From 156fe0d9fb74169e2bc9a4367708f4f963917cbf Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 12:06:39 +0800
Subject: [PATCH 080/298] fix: Optimize panel management, support panel
references for sidebar and tab types
---
src/activate/registerCommands.ts | 37 ++++++++++----
src/api/providers/human-relay.ts | 51 ++-----------------
src/core/webview/ClineProvider.ts | 10 ++++
.../human-relay/HumanRelayDialog.tsx | 12 ++++-
4 files changed, 51 insertions(+), 59 deletions(-)
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 8cc895f291..b520e0cb8e 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -3,17 +3,32 @@ import delay from "delay"
import { ClineProvider } from "../core/webview/ClineProvider"
-// Add a global variable to store panel references
-let panel: vscode.WebviewPanel | undefined = undefined
+// Store panel references in both modes
+let sidebarPanel: vscode.WebviewView | undefined = undefined
+let tabPanel: vscode.WebviewPanel | undefined = undefined
-// Get the panel function for command access
-export function getPanel(): vscode.WebviewPanel | undefined {
- return panel
+/**
+ * Get the currently active panel
+ * @returns WebviewPanel或WebviewView
+ */
+export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined {
+ return tabPanel || sidebarPanel
}
-// Setting the function of the panel
-export function setPanel(newPanel: vscode.WebviewPanel | undefined): void {
- panel = newPanel
+/**
+ * Set panel references
+ */
+export function setPanel(
+ newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined,
+ type: "sidebar" | "tab",
+): void {
+ if (type === "sidebar") {
+ sidebarPanel = newPanel as vscode.WebviewView
+ tabPanel = undefined
+ } else {
+ tabPanel = newPanel as vscode.WebviewPanel
+ sidebarPanel = undefined
+ }
}
export type RegisterCommandOptions = {
@@ -100,8 +115,8 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit {
- setPanel(undefined)
+ setPanel(undefined, "tab")
})
// Lock the editor group so clicking on files doesn't open them over the panel
diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts
index 2911e24eaf..618ae847de 100644
--- a/src/api/providers/human-relay.ts
+++ b/src/api/providers/human-relay.ts
@@ -127,51 +127,10 @@ async function showHumanRelayDialog(promptText: string): Promise {
- // Wait for the panel to be created before showing the human relay dialog
- setTimeout(() => {
- vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
- requestId,
- promptText,
- })
- }, 500) // Allow some time for the panel to be created
- })
- } else {
- // If the panel already exists, directly show the dialog
- vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
- requestId,
- promptText,
- })
- }
-
- // Provide a temporary UI in case the WebView fails to load
- vscode.window
- .showInformationMessage(
- "Please paste the copied message to the AI, then copy the response back into the dialog",
- {
- modal: true,
- detail: "The message has been copied to the clipboard. If the dialog does not open, please try using the input box.",
- },
- "Use Input Box",
- )
- .then((selection) => {
- if (selection === "Use Input Box") {
- // Unregister the callback
- vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
-
- vscode.window
- .showInputBox({
- prompt: "Please paste the AI's response here",
- placeHolder: "Paste the AI's response here...",
- ignoreFocusOut: true,
- })
- .then((input) => {
- resolve(input || undefined)
- })
- }
- })
+ // Open the dialog box directly using the current panel
+ vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
+ requestId,
+ promptText,
+ })
})
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index e781a36dbe..c87406d3d4 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -7,6 +7,7 @@ import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import simpleGit from "simple-git"
+import { setPanel } from "../../activate/registerCommands"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
@@ -233,6 +234,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Resolving webview view")
this.view = webviewView
+ // Set panel reference according to webview type
+ if ("onDidChangeViewState" in webviewView) {
+ // Tag page type
+ setPanel(webviewView, "tab")
+ } else if ("onDidChangeVisibility" in webviewView) {
+ // Sidebar Type
+ setPanel(webviewView, "sidebar")
+ }
+
// Initialize sound enabled state
this.getState().then(({ soundEnabled }) => {
setSoundEnabled(soundEnabled ?? false)
diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
index ea306d11d7..61d4cbe213 100644
--- a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
+++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx
@@ -27,12 +27,20 @@ export const HumanRelayDialog: React.FC = ({
onCancel,
}) => {
const [response, setResponse] = React.useState("")
- const { onCopy } = useClipboard(promptText)
+ const { copy } = useClipboard()
const [isCopyClicked, setIsCopyClicked] = React.useState(false)
+ // Listen to isOpen changes, clear the input box when the dialog box is opened
+ React.useEffect(() => {
+ if (isOpen) {
+ setResponse("")
+ setIsCopyClicked(false)
+ }
+ }, [isOpen])
+
// Copy to clipboard and show a success message
const handleCopy = () => {
- onCopy()
+ copy(promptText)
setIsCopyClicked(true)
setTimeout(() => {
setIsCopyClicked(false)
From c04adc6dcb5c7711e3b17a75fd056f12b76397f5 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Fri, 28 Feb 2025 06:19:13 +0200
Subject: [PATCH 081/298] added error message back to the parent task in cases
user stops, cancel or delete the subtask, to allow the parent task to resume
and think how to continue
---
src/core/Cline.ts | 5 +----
src/core/webview/ClineProvider.ts | 11 ++++++++++-
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index d05f9320d4..9cb0228e3e 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -574,10 +574,7 @@ export class Cline {
try {
// This adds the completion message to conversation history
- await this.say(
- "text",
- `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`,
- )
+ await this.say("text", `${lastMessage ?? "Please continue to the next task."}`)
await this.addToApiConversationHistory({
role: "user",
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 4249081277..f92650f887 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -223,7 +223,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// remove the last cline instance from the stack (this is the finished sub task)
await this.removeClineFromStack()
// resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
- this.getCurrentCline()?.resumePausedTask(lastMessage)
+ this.getCurrentCline()?.resumePausedTask(`new_task finished successfully! ${lastMessage}`)
} catch (error) {
this.log(`Error in finishSubTask: ${error.message}`)
throw error
@@ -925,6 +925,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "clearTask":
// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
await this.removeClineFromStack()
+ // resume previouse task with subtask failed error
+ this.getCurrentCline()?.resumePausedTask(
+ `new_task finished with an error!, it was stopped and canceled by the user.`,
+ )
+
await this.postStateToWebview()
break
case "didShowAnnouncement":
@@ -2083,6 +2088,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// remove task from stack if it's the current task
if (id === this.getCurrentCline()?.taskId) {
await this.removeClineWithIdFromStack(id)
+ // resume previouse task with subtask failed error
+ this.getCurrentCline()?.resumePausedTask(
+ `new_task finished with an error!, it was stopped and delted by the user.`,
+ )
}
// delete task from the task history state
From db65520adb862e52a8e0f30ecb7e003e3e878959 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 12:32:06 +0800
Subject: [PATCH 082/298] fix: Fixed human relay dialog message processing,
optimized type use
---
webview-ui/src/App.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 5909a3eaef..8c37236adb 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
+import { ShowHumanRelayDialogMessage } from "../../src/shared/ExtensionMessage"
import { vscode } from "./utils/vscode"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
@@ -59,13 +60,13 @@ const App = () => {
switchTab(newTab)
}
}
-
+ const mes: ShowHumanRelayDialogMessage = message as ShowHumanRelayDialogMessage
// Processing displays human relay dialog messages
- if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) {
+ if (mes.type === "showHumanRelayDialog" && mes.requestId && mes.promptText) {
setHumanRelayDialogState({
isOpen: true,
- requestId: message.requestId,
- promptText: message.promptText,
+ requestId: mes.requestId,
+ promptText: mes.promptText,
})
}
},
From 21fed4cb799ff8397d5d1f1348252156dfcbbf71 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Thu, 27 Feb 2025 22:26:13 -0800
Subject: [PATCH 083/298] Delete task confirmation enhancements
---
.changeset/chilly-bugs-pay.md | 5 ++
webview-ui/src/components/chat/TaskHeader.tsx | 86 +++++++++++++------
.../components/history/DeleteTaskDialog.tsx | 42 ++++++---
.../src/components/history/HistoryView.tsx | 30 +++----
.../history/__tests__/HistoryView.test.tsx | 60 +++++++++----
5 files changed, 149 insertions(+), 74 deletions(-)
create mode 100644 .changeset/chilly-bugs-pay.md
diff --git a/.changeset/chilly-bugs-pay.md b/.changeset/chilly-bugs-pay.md
new file mode 100644
index 0000000000..b30f8241ef
--- /dev/null
+++ b/.changeset/chilly-bugs-pay.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Delete task confirmation enhancements
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index fb7db6f617..319a9aeccd 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -3,16 +3,19 @@ import { useWindowSize } from "react-use"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import prettyBytes from "pretty-bytes"
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber } from "@/utils/format"
+import { Button } from "@/components/ui"
+
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
-import Thumbnails from "../common/Thumbnails"
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
-import { formatLargeNumber } from "../../utils/format"
-import { normalizeApiConfiguration } from "../settings/ApiOptions"
-import { Button } from "../ui"
import { HistoryItem } from "../../../../src/shared/HistoryItem"
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import Thumbnails from "../common/Thumbnails"
+import { normalizeApiConfiguration } from "../settings/ApiOptions"
+import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
+
interface TaskHeaderProps {
task: ClineMessage
tokensIn: number
@@ -46,7 +49,21 @@ const TaskHeader: React.FC = ({
const contextWindow = selectedModelInfo?.contextWindow || 1
/*
- When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
+ When dealing with event listeners in React components that depend on state
+ variables, we face a challenge. We want our listener to always use the most
+ up-to-date version of a callback function that relies on current state, but
+ we don't want to constantly add and remove event listeners as that function
+ updates. This scenario often arises with resize listeners or other window
+ events. Simply adding the listener in a useEffect with an empty dependency
+ array risks using stale state, while including the callback in the
+ dependencies can lead to unnecessary re-registrations of the listener. There
+ are react hook libraries that provide a elegant solution to this problem by
+ utilizing the useRef hook to maintain a reference to the latest callback
+ function without triggering re-renders or effect re-runs. This approach
+ ensures that our event listener always has access to the most current state
+ while minimizing performance overhead and potential memory leaks from
+ multiple listener registrations.
+
Sources
- https://usehooks-ts.com/react-hook/use-event-listener
- https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
@@ -350,27 +367,48 @@ export const highlightMentions = (text?: string, withShadow = true) => {
})
}
-const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
-
-
vscode.postMessage({ type: "exportCurrentTask" })}>
-
-
- {!!item?.size && item.size > 0 && (
+const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
+ const [deleteTaskId, setDeleteTaskId] = useState
(null)
+
+ return (
+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
-
- {prettyBytes(item.size)}
+ title="Export task history"
+ onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
+
- )}
-
-)
+ {!!item?.size && item.size > 0 && (
+ <>
+ {
+ e.stopPropagation()
+
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
+ } else {
+ setDeleteTaskId(item.id)
+ }
+ }}>
+
+ {prettyBytes(item.size)}
+
+ {deleteTaskId && (
+ !open && setDeleteTaskId(null)}
+ open
+ />
+ )}
+ >
+ )}
+
+ )
+}
const ContextWindowProgress = ({ contextWindow, contextTokens }: { contextWindow: number; contextTokens: number }) => (
<>
diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx
index b40adeae3d..31d85abd37 100644
--- a/webview-ui/src/components/history/DeleteTaskDialog.tsx
+++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx
@@ -1,4 +1,7 @@
-import React from "react"
+import { useCallback, useEffect } from "react"
+import { useKeyPress } from "react-use"
+import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
+
import {
AlertDialog,
AlertDialogAction,
@@ -8,25 +11,36 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Button } from "@/components/ui"
+ Button,
+} from "@/components/ui"
+
import { vscode } from "@/utils/vscode"
-interface DeleteTaskDialogProps {
+interface DeleteTaskDialogProps extends AlertDialogProps {
taskId: string
- open: boolean
- onOpenChange: (open: boolean) => void
}
-export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialogProps) => {
- const handleDelete = () => {
- vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
- onOpenChange(false)
- }
+export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
+ const [isEnterPressed] = useKeyPress("Enter")
+
+ const { onOpenChange } = props
+
+ const onDelete = useCallback(() => {
+ if (taskId) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
+ onOpenChange?.(false)
+ }
+ }, [taskId, onOpenChange])
+
+ useEffect(() => {
+ if (taskId && isEnterPressed) {
+ onDelete()
+ }
+ }, [taskId, isEnterPressed, onDelete])
return (
-
-
+
+ onOpenChange?.(false)}>
Delete Task
@@ -38,7 +52,7 @@ export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialo
Cancel
-
+
Delete
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index ca60e1fcb8..49d71e5ddd 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -38,13 +38,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
- const [taskToDelete, setTaskToDelete] = useState(null)
-
- const handleDeleteHistoryItem = (id: string) => {
- setTaskToDelete(id)
- setDeleteDialogOpen(true)
- }
+ const [deleteTaskId, setDeleteTaskId] = useState(null)
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
@@ -230,10 +224,15 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{
e.stopPropagation()
- handleDeleteHistoryItem(item.id)
+
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
+ } else {
+ setDeleteTaskId(item.id)
+ }
}}>
{item.size && prettyBytes(item.size)}
@@ -403,17 +402,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)}
/>
- {taskToDelete && (
-
{
- setDeleteDialogOpen(open)
- if (!open) {
- setTaskToDelete(null)
- }
- }}
- />
+ {deleteTaskId && (
+ !open && setDeleteTaskId(null)} open />
)}
)
diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
index 12b0181af6..4b761d6fc4 100644
--- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
+++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
@@ -135,26 +135,54 @@ describe("HistoryView", () => {
})
})
- it("handles task deletion", async () => {
- const onDone = jest.fn()
- render()
+ describe("task deletion", () => {
+ it("shows confirmation dialog on regular click", () => {
+ const onDone = jest.fn()
+ render()
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
+ // Find and hover over first task
+ const taskContainer = screen.getByTestId("virtuoso-item-1")
+ fireEvent.mouseEnter(taskContainer)
- // Click delete button to open confirmation dialog
- const deleteButton = within(taskContainer).getByTitle("Delete Task")
- fireEvent.click(deleteButton)
+ // Click delete button to open confirmation dialog
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton)
- // Find and click the confirm delete button in the dialog
- const confirmDeleteButton = screen.getByRole("button", { name: /delete/i })
- fireEvent.click(confirmDeleteButton)
+ // Verify dialog is shown
+ const dialog = screen.getByRole("alertdialog")
+ expect(dialog).toBeInTheDocument()
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "deleteTaskWithId",
- text: "1",
+ // Find and click the confirm delete button in the dialog
+ const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
+ fireEvent.click(confirmDeleteButton)
+
+ // Verify vscode message was sent
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: "1",
+ })
+ })
+
+ it("deletes immediately on shift-click without confirmation", () => {
+ const onDone = jest.fn()
+ render()
+
+ // Find and hover over first task
+ const taskContainer = screen.getByTestId("virtuoso-item-1")
+ fireEvent.mouseEnter(taskContainer)
+
+ // Shift-click delete button
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton, { shiftKey: true })
+
+ // Verify no dialog is shown
+ expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
+
+ // Verify vscode message was sent
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: "1",
+ })
})
})
From aceeb0b5c5fd20cc2bb90f08e2dd66d12e584b06 Mon Sep 17 00:00:00 2001
From: Felix NyxJae <18661811993@163.com>
Date: Fri, 28 Feb 2025 14:53:54 +0800
Subject: [PATCH 084/298] chore: Restore .gitignore, remove unnecessary file
rules
---
.gitignore | 5 -----
1 file changed, 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index bdae7b5b26..211d06aa19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,8 +28,3 @@ docs/_site/
#Logging
logs
-.clinerules-architect
-.clinerules-ask
-.clinerules-code
-MemoryBank
-.github/copilot-instructions.md
From b3fd1a2e232f059b9ba6d480795445624ff8206c Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 09:08:58 -0800
Subject: [PATCH 085/298] Prettier thinking blocks
---
webview-ui/src/components/chat/ChatRow.tsx | 59 +++------
.../src/components/chat/ReasoningBlock.tsx | 117 +++++++++++-------
webview-ui/src/index.css | 2 +
3 files changed, 89 insertions(+), 89 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index 4017ccf318..1533bba3a8 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -16,7 +16,7 @@ import { vscode } from "../../utils/vscode"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
-import ReasoningBlock from "./ReasoningBlock"
+import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
@@ -25,12 +25,12 @@ import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
interface ChatRowProps {
message: ClineMessage
- isExpanded: boolean
- onToggleExpand: () => void
lastModifiedMessage?: ClineMessage
+ isExpanded: boolean
isLast: boolean
- onHeightChange: (isTaller: boolean) => void
isStreaming: boolean
+ onToggleExpand: () => void
+ onHeightChange: (isTaller: boolean) => void
}
interface ChatRowContentProps extends Omit {}
@@ -43,10 +43,7 @@ const ChatRow = memo(
const prevHeightRef = useRef(0)
const [chatrow, { height }] = useSize(
-
+
,
)
@@ -75,33 +72,32 @@ export default ChatRow
export const ChatRowContent = ({
message,
- isExpanded,
- onToggleExpand,
lastModifiedMessage,
+ isExpanded,
isLast,
isStreaming,
+ onToggleExpand,
}: ChatRowContentProps) => {
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
- const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
+ const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
- // Auto-collapse reasoning when new messages arrive
- useEffect(() => {
- if (!isLast && message.say === "reasoning") {
- setReasoningCollapsed(true)
- }
- }, [isLast, message.say])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage]
}
+
return [undefined, undefined, undefined]
}, [message.text, message.say])
- // when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
+
+ // When resuming task, last wont be api_req_failed but a resume_task
+ // message, so api_req_started will show loading spinner. That's why we just
+ // remove the last api_req_started that failed without streaming anything.
const apiRequestFailedMessage =
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
? lastModifiedMessage?.text
: undefined
+
const isCommandExecuting =
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
@@ -428,32 +424,6 @@ export const ChatRowContent = ({
/>
>
)
- // case "inspectSite":
- // const isInspecting =
- // isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images
- // return (
- // <>
- //
- // {isInspecting ?
: toolIcon("inspect")}
- //
- // {message.type === "ask" ? (
- // <>Roo wants to inspect this website:>
- // ) : (
- // <>Roo is inspecting this website:>
- // )}
- //
- //
- //
- //
- //
- // >
- // )
case "switchMode":
return (
<>
@@ -501,6 +471,7 @@ export const ChatRowContent = ({
return (
setReasoningCollapsed(!reasoningCollapsed)}
/>
diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx
index 0c9971f269..fa12899092 100644
--- a/webview-ui/src/components/chat/ReasoningBlock.tsx
+++ b/webview-ui/src/components/chat/ReasoningBlock.tsx
@@ -1,70 +1,97 @@
-import React, { useEffect, useRef } from "react"
-import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
+import { useCallback, useEffect, useRef, useState } from "react"
+import { CaretDownIcon, CaretUpIcon, CounterClockwiseClockIcon } from "@radix-ui/react-icons"
+
import MarkdownBlock from "../common/MarkdownBlock"
+import { useMount } from "react-use"
interface ReasoningBlockProps {
content: string
+ elapsed?: number
isCollapsed?: boolean
onToggleCollapse?: () => void
- autoHeight?: boolean
}
-const ReasoningBlock: React.FC = ({
- content,
- isCollapsed = false,
- onToggleCollapse,
- autoHeight = false,
-}) => {
+export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggleCollapse }: ReasoningBlockProps) => {
const contentRef = useRef(null)
+ const elapsedRef = useRef(0)
+ const [thought, setThought] = useState()
+ const [prevThought, setPrevThought] = useState("Thinking")
+ const [isTransitioning, setIsTransitioning] = useState(false)
+ const cursorRef = useRef(0)
+ const queueRef = useRef([])
- // Scroll to bottom when content updates
useEffect(() => {
if (contentRef.current && !isCollapsed) {
contentRef.current.scrollTop = contentRef.current.scrollHeight
}
}, [content, isCollapsed])
+ useEffect(() => {
+ if (elapsed) {
+ elapsedRef.current = elapsed
+ }
+ }, [elapsed])
+
+ // Process the transition queue.
+ const processNextTransition = useCallback(() => {
+ const nextThought = queueRef.current.pop()
+ queueRef.current = []
+
+ if (nextThought) {
+ setIsTransitioning(true)
+ }
+
+ setTimeout(() => {
+ if (nextThought) {
+ setPrevThought(nextThought)
+ setIsTransitioning(false)
+ }
+
+ setTimeout(() => processNextTransition(), 500)
+ }, 200)
+ }, [])
+
+ useMount(() => {
+ processNextTransition()
+ })
+
+ useEffect(() => {
+ if (content.length - cursorRef.current > 160) {
+ setThought("... " + content.slice(cursorRef.current))
+ cursorRef.current = content.length
+ }
+ }, [content])
+
+ useEffect(() => {
+ if (thought && thought !== prevThought) {
+ queueRef.current.push(thought)
+ }
+ }, [thought, prevThought])
+
return (
-
+
-
Reasoning
-
+ className="flex items-center justify-between gap-1 px-3 py-2 cursor-pointer text-muted-foreground"
+ onClick={onToggleCollapse}>
+
+ {prevThought}
+
+
+ {elapsedRef.current > 1000 && (
+ <>
+
+
{Math.round(elapsedRef.current / 1000)}s
+ >
+ )}
+ {isCollapsed ?
:
}
+
{!isCollapsed && (
-
)
}
-
-export default ReasoningBlock
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 53025be01a..74c8463b37 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -64,6 +64,8 @@
--color-vscode-editor-foreground: var(--vscode-editor-foreground);
--color-vscode-editor-background: var(--vscode-editor-background);
+ --color-vscode-editorGroup-border: var(--vscode-editorGroup-border);
+
--color-vscode-button-foreground: var(--vscode-button-foreground);
--color-vscode-button-background: var(--vscode-button-background);
--color-vscode-button-secondaryForeground: var(--vscode-button-secondaryForeground);
From 360e47d641e9acbb33586342a079efaf150ef85f Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 09:09:42 -0800
Subject: [PATCH 086/298] Add changeset
---
.changeset/young-hornets-taste.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/young-hornets-taste.md
diff --git a/.changeset/young-hornets-taste.md b/.changeset/young-hornets-taste.md
new file mode 100644
index 0000000000..1b9c3d94e8
--- /dev/null
+++ b/.changeset/young-hornets-taste.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Prettier thinking blocks
From 9b30065231061dfbcae9d8ef6e14082fe9064757 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 08:25:04 -0800
Subject: [PATCH 087/298] Make the copy action on history and history preview
consistent
---
.../src/components/history/CopyButton.tsx | 32 +++++
.../src/components/history/ExportButton.tsx | 16 +++
.../src/components/history/HistoryPreview.tsx | 124 ++++--------------
.../src/components/history/HistoryView.tsx | 51 +------
webview-ui/src/index.css | 2 +
webview-ui/src/utils/__tests__/format.test.ts | 51 +++++++
webview-ui/src/utils/format.ts | 15 +++
7 files changed, 147 insertions(+), 144 deletions(-)
create mode 100644 webview-ui/src/components/history/CopyButton.tsx
create mode 100644 webview-ui/src/components/history/ExportButton.tsx
create mode 100644 webview-ui/src/utils/__tests__/format.test.ts
diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx
new file mode 100644
index 0000000000..0e693b4470
--- /dev/null
+++ b/webview-ui/src/components/history/CopyButton.tsx
@@ -0,0 +1,32 @@
+import { useCallback } from "react"
+
+import { useClipboard } from "@/components/ui/hooks"
+import { Button } from "@/components/ui"
+import { cn } from "@/lib/utils"
+
+type CopyButtonProps = {
+ itemTask: string
+}
+
+export const CopyButton = ({ itemTask }: CopyButtonProps) => {
+ const { isCopied, copy } = useClipboard()
+
+ const onCopy = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ !isCopied && copy(itemTask)
+ },
+ [isCopied, copy, itemTask],
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx
new file mode 100644
index 0000000000..6617e475bd
--- /dev/null
+++ b/webview-ui/src/components/history/ExportButton.tsx
@@ -0,0 +1,16 @@
+import { vscode } from "@/utils/vscode"
+import { Button } from "@/components/ui"
+
+export const ExportButton = ({ itemId }: { itemId: string }) => (
+ {
+ e.stopPropagation()
+ vscode.postMessage({ type: "exportTaskWithId", text: itemId })
+ }}>
+
+
+)
diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx
index b2898fc6a8..bf53845da7 100644
--- a/webview-ui/src/components/history/HistoryPreview.tsx
+++ b/webview-ui/src/components/history/HistoryPreview.tsx
@@ -1,9 +1,11 @@
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
import { memo } from "react"
-import { formatLargeNumber } from "../../utils/format"
-import { useCopyToClipboard } from "../../utils/clipboard"
+
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber, formatDate } from "@/utils/format"
+import { Button } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { CopyButton } from "./CopyButton"
type HistoryPreviewProps = {
showHistoryView: () => void
@@ -11,52 +13,15 @@ type HistoryPreviewProps = {
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
- const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
+
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
- const formatDate = (timestamp: number) => {
- const date = new Date(timestamp)
- return date
- ?.toLocaleString("en-US", {
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- })
- .replace(", ", " ")
- .replace(" at", ",")
- .toUpperCase()
- }
-
return (
- {showCopyFeedback &&
Prompt Copied to Clipboard
}
-
{
display: "flex",
alignItems: "center",
}}>
-
-
- Recent Tasks
-
+
+ Recent Tasks
-
-
+
{taskHistory
.filter((item) => item.ts && item.task)
.slice(0, 3)
@@ -103,48 +57,25 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
key={item.id}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
-
-
-
+
+
+
{formatDate(item.ts)}
- copyWithFeedback(item.task, e)}>
-
-
+
{item.task}
-
+
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
{formatLargeNumber(item.tokensOut || 0)}
@@ -168,21 +99,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
))}
-
-
+ showHistoryView()}
- style={{
- opacity: 0.9,
- }}>
-
- View all history
-
-
+ className="font-normal text-vscode-descriptionForeground">
+ View all history
+
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index 49d71e5ddd..d50a569c8d 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -5,12 +5,14 @@ import prettyBytes from "pretty-bytes"
import { Virtuoso } from "react-virtuoso"
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
+import { vscode } from "@/utils/vscode"
+import { formatLargeNumber, formatDate } from "@/utils/format"
+import { highlightFzfMatch } from "@/utils/highlight"
+import { Button } from "@/components/ui"
+
import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
-import { formatLargeNumber } from "../../utils/format"
-import { highlightFzfMatch } from "../../utils/highlight"
-import { useCopyToClipboard } from "../../utils/clipboard"
-import { Button } from "../ui"
+import { ExportButton } from "./ExportButton"
+import { CopyButton } from "./CopyButton"
type HistoryViewProps = {
onDone: () => void
@@ -40,21 +42,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [deleteTaskId, setDeleteTaskId] = useState
(null)
- const formatDate = (timestamp: number) => {
- const date = new Date(timestamp)
- return date
- ?.toLocaleString("en-US", {
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- })
- .replace(", ", " ")
- .replace(" at", ",")
- .toUpperCase()
- }
-
const presentableTasks = useMemo(() => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
@@ -409,28 +396,4 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)
}
-const CopyButton = ({ itemTask }: { itemTask: string }) => {
- const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
-
- return (
- copyWithFeedback(itemTask, e)}>
- {showCopyFeedback ? : }
-
- )
-}
-
-const ExportButton = ({ itemId }: { itemId: string }) => (
- {
- e.stopPropagation()
- vscode.postMessage({ type: "exportTaskWithId", text: itemId })
- }}>
-
-
-)
-
export default memo(HistoryView)
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 53025be01a..0e80d1b0c6 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -23,6 +23,8 @@
@theme {
--font-display: var(--vscode-font-family);
+
+ --text-xs: calc(var(--vscode-font-size) * 0.85);
--text-sm: calc(var(--vscode-font-size) * 0.9);
--text-base: var(--vscode-font-size);
--text-lg: calc(var(--vscode-font-size) * 1.1);
diff --git a/webview-ui/src/utils/__tests__/format.test.ts b/webview-ui/src/utils/__tests__/format.test.ts
new file mode 100644
index 0000000000..7377874fd0
--- /dev/null
+++ b/webview-ui/src/utils/__tests__/format.test.ts
@@ -0,0 +1,51 @@
+// npx jest src/utils/__tests__/format.test.ts
+
+import { formatDate } from "../format"
+
+describe("formatDate", () => {
+ it("formats a timestamp correctly", () => {
+ // January 15, 2023, 10:30 AM
+ const timestamp = new Date(2023, 0, 15, 10, 30).getTime()
+ const result = formatDate(timestamp)
+
+ expect(result).toBe("JANUARY 15, 10:30 AM")
+ })
+
+ it("handles different months correctly", () => {
+ // February 28, 2023, 3:45 PM
+ const timestamp1 = new Date(2023, 1, 28, 15, 45).getTime()
+ expect(formatDate(timestamp1)).toBe("FEBRUARY 28, 3:45 PM")
+
+ // December 31, 2023, 11:59 PM
+ const timestamp2 = new Date(2023, 11, 31, 23, 59).getTime()
+ expect(formatDate(timestamp2)).toBe("DECEMBER 31, 11:59 PM")
+ })
+
+ it("handles AM/PM correctly", () => {
+ // Morning time - 7:05 AM
+ const morningTimestamp = new Date(2023, 5, 15, 7, 5).getTime()
+ expect(formatDate(morningTimestamp)).toBe("JUNE 15, 7:05 AM")
+
+ // Noon - 12:00 PM
+ const noonTimestamp = new Date(2023, 5, 15, 12, 0).getTime()
+ expect(formatDate(noonTimestamp)).toBe("JUNE 15, 12:00 PM")
+
+ // Evening time - 8:15 PM
+ const eveningTimestamp = new Date(2023, 5, 15, 20, 15).getTime()
+ expect(formatDate(eveningTimestamp)).toBe("JUNE 15, 8:15 PM")
+ })
+
+ it("handles single-digit minutes with leading zeros", () => {
+ // 9:05 AM
+ const timestamp = new Date(2023, 3, 10, 9, 5).getTime()
+ expect(formatDate(timestamp)).toBe("APRIL 10, 9:05 AM")
+ })
+
+ it("converts the result to uppercase", () => {
+ const timestamp = new Date(2023, 8, 21, 16, 45).getTime()
+ const result = formatDate(timestamp)
+
+ expect(result).toBe(result.toUpperCase())
+ expect(result).toBe("SEPTEMBER 21, 4:45 PM")
+ })
+})
diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts
index 2e473c9b8a..12e9996205 100644
--- a/webview-ui/src/utils/format.ts
+++ b/webview-ui/src/utils/format.ts
@@ -10,3 +10,18 @@ export function formatLargeNumber(num: number): string {
}
return num.toString()
}
+
+export const formatDate = (timestamp: number) => {
+ const date = new Date(timestamp)
+ return date
+ .toLocaleString("en-US", {
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ })
+ .replace(", ", " ")
+ .replace(" at", ",")
+ .toUpperCase()
+}
From 31d2d17847abdb30cf1f7430aa5ef63df9f3eea9 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 01:15:44 -0800
Subject: [PATCH 088/298] Fix maxTokens defaults for Claude 3.7 Sonnet models
---
.changeset/tasty-grapes-suffer.md | 5 ++++
src/api/providers/openrouter.ts | 2 +-
src/shared/api.ts | 6 ++---
.../src/components/settings/ApiOptions.tsx | 2 --
.../components/settings/ThinkingBudget.tsx | 12 +++------
.../settings/__tests__/ApiOptions.test.tsx | 2 --
.../__tests__/ThinkingBudget.test.tsx | 26 +++----------------
7 files changed, 15 insertions(+), 40 deletions(-)
create mode 100644 .changeset/tasty-grapes-suffer.md
diff --git a/.changeset/tasty-grapes-suffer.md b/.changeset/tasty-grapes-suffer.md
new file mode 100644
index 0000000000..7382b38c77
--- /dev/null
+++ b/.changeset/tasty-grapes-suffer.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Fix maxTokens defaults for Claude 3.7 Sonnet models
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 82c02e20a7..70c40c2c27 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -278,7 +278,7 @@ export async function getOpenRouterModels() {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
- modelInfo.maxTokens = 64_000
+ modelInfo.maxTokens = rawModel.id === "anthropic/claude-3.7-sonnet:thinking" ? 64_000 : 16_384
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
modelInfo.supportsPromptCache = true
diff --git a/src/shared/api.ts b/src/shared/api.ts
index f88bb5e8b5..99e2986e88 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -111,7 +111,7 @@ export const anthropicModels = {
thinking: true,
},
"claude-3-7-sonnet-20250219": {
- maxTokens: 64_000,
+ maxTokens: 16_384,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
@@ -437,7 +437,7 @@ export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
export const vertexModels = {
"claude-3-7-sonnet@20250219:thinking": {
- maxTokens: 64000,
+ maxTokens: 64_000,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
@@ -449,7 +449,7 @@ export const vertexModels = {
thinking: true,
},
"claude-3-7-sonnet@20250219": {
- maxTokens: 8192,
+ maxTokens: 16_384,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 42ac5cdcb3..6f0dba9f00 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -7,7 +7,6 @@ import * as vscodemodels from "vscode"
import {
ApiConfiguration,
ModelInfo,
- ApiProvider,
anthropicDefaultModelId,
anthropicModels,
azureOpenAiDefaultApiVersion,
@@ -1385,7 +1384,6 @@ const ApiOptions = ({
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
modelInfo={selectedModelInfo}
- provider={selectedProvider as ApiProvider}
/>
(field: K, value: ApiConfiguration[K]) => void
modelInfo?: ModelInfo
- provider?: ApiProvider
}
-export const ThinkingBudget = ({
- apiConfiguration,
- setApiConfigurationField,
- modelInfo,
- provider,
-}: ThinkingBudgetProps) => {
- const tokens = apiConfiguration?.modelMaxTokens || modelInfo?.maxTokens || 64_000
+export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+ const tokens = apiConfiguration?.modelMaxTokens || 16_384
const tokensMin = 8192
const tokensMax = modelInfo?.maxTokens || 64_000
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 06ed95585a..0b1fb28498 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -92,7 +92,6 @@ describe("ApiOptions", () => {
})
expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
- expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "anthropic")
})
it("should show ThinkingBudget for Vertex models that support thinking", () => {
@@ -104,7 +103,6 @@ describe("ApiOptions", () => {
})
expect(screen.getByTestId("thinking-budget")).toBeInTheDocument()
- expect(screen.getByTestId("thinking-budget")).toHaveAttribute("data-provider", "vertex")
})
it("should not show ThinkingBudget for models that don't support thinking", () => {
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
index 212316ea9a..1e14e94524 100644
--- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.test.tsx
@@ -1,7 +1,6 @@
-import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { ThinkingBudget } from "../ThinkingBudget"
-import { ApiProvider, ModelInfo } from "../../../../../src/shared/api"
+import { ModelInfo } from "../../../../../src/shared/api"
// Mock Slider component
jest.mock("@/components/ui", () => ({
@@ -25,11 +24,11 @@ describe("ThinkingBudget", () => {
supportsPromptCache: true,
supportsImages: true,
}
+
const defaultProps = {
apiConfiguration: {},
setApiConfigurationField: jest.fn(),
modelInfo: mockModelInfo,
- provider: "anthropic" as ApiProvider,
}
beforeEach(() => {
@@ -60,7 +59,7 @@ describe("ThinkingBudget", () => {
expect(screen.getAllByTestId("slider")).toHaveLength(2)
})
- it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
+ it("should update modelMaxThinkingTokens", () => {
const setApiConfigurationField = jest.fn()
render(
@@ -68,25 +67,6 @@ describe("ThinkingBudget", () => {
{...defaultProps}
apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
setApiConfigurationField={setApiConfigurationField}
- provider="anthropic"
- />,
- )
-
- const sliders = screen.getAllByTestId("slider")
- fireEvent.change(sliders[1], { target: { value: "5000" } })
-
- expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", 5000)
- })
-
- it("should use modelMaxThinkingTokens field for Vertex provider", () => {
- const setApiConfigurationField = jest.fn()
-
- render(
- ,
)
From 0a81853d84e43ca92718166db6d07ea984889a39 Mon Sep 17 00:00:00 2001
From: yansheng3
Date: Fri, 28 Feb 2025 19:06:43 +0800
Subject: [PATCH 089/298] Optimize Task ID Initialization Order
Signed-off-by: yansheng3
---
src/core/Cline.ts | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 00897eecf4..3127d3d38b 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -148,7 +148,8 @@ export class Cline {
throw new Error("Either historyItem or task/images must be provided")
}
- this.taskId = crypto.randomUUID()
+ this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
+
this.apiConfiguration = apiConfiguration
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
@@ -161,10 +162,6 @@ export class Cline {
this.diffViewProvider = new DiffViewProvider(cwd)
this.enableCheckpoints = enableCheckpoints ?? false
- if (historyItem) {
- this.taskId = historyItem.id
- }
-
// Initialize diffStrategy based on current state
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
From 2773d391535db0f03dedc1df13af809b3f23c500 Mon Sep 17 00:00:00 2001
From: samir-nimbly <112695483+samir-nimbly@users.noreply.github.com>
Date: Fri, 28 Feb 2025 20:05:31 +0530
Subject: [PATCH 090/298] fix: dropdown hover color fix for white themes
---
webview-ui/src/components/chat/ContextMenu.tsx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx
index 2bb7a8ee68..20bd5222f6 100644
--- a/webview-ui/src/components/chat/ContextMenu.tsx
+++ b/webview-ui/src/components/chat/ContextMenu.tsx
@@ -187,10 +187,12 @@ const ContextMenu: React.FC = ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
- backgroundColor:
- index === selectedIndex && isOptionSelectable(option)
- ? "var(--vscode-list-activeSelectionBackground)"
- : "",
+ ...(index === selectedIndex && isOptionSelectable(option)
+ ? {
+ backgroundColor: "var(--vscode-list-activeSelectionBackground)",
+ color: "var(--vscode-list-activeSelectionForeground)",
+ }
+ : {}),
}}
onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}>
Date: Fri, 28 Feb 2025 10:30:37 -0500
Subject: [PATCH 091/298] Update pull_request_template.md
---
.github/pull_request_template.md | 42 +++++++++++++++-----------------
1 file changed, 20 insertions(+), 22 deletions(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 7ee8bb98ad..85b5c9063c 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,37 +1,35 @@
-
+## Context
-## Description
+
-## Type of change
+## Implementation
-
+
-
+## Screenshots
-## Checklist:
+| before | after |
+| ------ | ----- |
+| | |
-
+## How to Test
-- [ ] My code follows the patterns of this project
-- [ ] I have performed a self-review of my own code
-- [ ] I have commented my code, particularly in hard-to-understand areas
-- [ ] I have made corresponding changes to the documentation
+
+A "How To Test" section can look something like this:
-## Related Issues
+- Sign in with a user with tracks
+- Activate `show_awesome_cat_gifs` feature (add `?feature.show_awesome_cat_gifs=1` to your URL)
+- You should see a GIF with cats dancing
-
+-->
-## Reviewers
+## Get in Touch
-
+
From 87785bb7504710ddda21bb3693090e6a546a77ff Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Fri, 28 Feb 2025 10:35:38 -0500
Subject: [PATCH 092/298] Update pull_request_template.md
---
.github/pull_request_template.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 85b5c9063c..de7e461cb9 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -32,4 +32,4 @@ A "How To Test" section can look something like this:
## Get in Touch
-
+
From 4b66ce8255b594a94903b3859a29ad25244c3f3f Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 09:38:48 -0800
Subject: [PATCH 093/298] Improve terminal command execution output parsing
---
src/core/Cline.ts | 28 +-
src/integrations/terminal/TerminalManager.ts | 193 +++++++-
src/integrations/terminal/TerminalProcess.ts | 427 ++++++++++++------
src/integrations/terminal/TerminalRegistry.ts | 30 ++
.../__tests__/TerminalProcess.test.ts | 163 +++----
.../__tests__/TerminalRegistry.test.ts | 5 +-
src/shared/combineCommandSequences.ts | 2 +-
7 files changed, 591 insertions(+), 257 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 00897eecf4..fab181b86e 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -22,7 +22,7 @@ import {
everyLineHasLineNumbers,
truncateOutput,
} from "../integrations/misc/extract-text"
-import { TerminalManager } from "../integrations/terminal/TerminalManager"
+import { TerminalManager, ExitCodeDetails } from "../integrations/terminal/TerminalManager"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
@@ -834,10 +834,21 @@ export class Cline {
})
let completed = false
- process.once("completed", () => {
+ let exitDetails: ExitCodeDetails | undefined
+ process.once("completed", (output?: string) => {
+ // Use provided output if available, otherwise keep existing result.
+ if (output) {
+ lines = output.split("\n")
+ }
completed = true
})
+ process.once("shell_execution_complete", (id: number, details: ExitCodeDetails) => {
+ if (id === terminalInfo.id) {
+ exitDetails = details
+ }
+ })
+
process.once("no_shell_integration", async () => {
await this.say("shell_integration_warning")
})
@@ -869,7 +880,18 @@ export class Cline {
}
if (completed) {
- return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
+ let exitStatus = "No exit code available"
+ if (exitDetails !== undefined) {
+ if (exitDetails.signal) {
+ exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})`
+ if (exitDetails.coreDumpPossible) {
+ exitStatus += " - core dump possible"
+ }
+ } else {
+ exitStatus = `Exit code: ${exitDetails.exitCode}`
+ }
+ }
+ return [false, `Command executed. ${exitStatus}${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
} else {
return [
false,
diff --git a/src/integrations/terminal/TerminalManager.ts b/src/integrations/terminal/TerminalManager.ts
index d5496e20fb..a55f7867d4 100644
--- a/src/integrations/terminal/TerminalManager.ts
+++ b/src/integrations/terminal/TerminalManager.ts
@@ -70,6 +70,15 @@ Interestingly, some environments like Cursor enable these APIs even without the
This approach allows us to leverage advanced features when available while ensuring broad compatibility.
*/
declare module "vscode" {
+ // https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L7442
+ // interface Terminal {
+ // shellIntegration?: {
+ // cwd?: vscode.Uri
+ // executeCommand?: (command: string) => {
+ // read: () => AsyncIterable
+ // }
+ // }
+ // }
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L10794
interface Window {
onDidStartTerminalShellExecution?: (
@@ -77,17 +86,19 @@ declare module "vscode" {
thisArgs?: any,
disposables?: vscode.Disposable[],
) => vscode.Disposable
+ onDidEndTerminalShellExecution?: (
+ listener: (e: { terminal: vscode.Terminal; exitCode?: number; shellType?: string }) => any,
+ thisArgs?: any,
+ disposables?: vscode.Disposable[],
+ ) => vscode.Disposable
}
}
-// Extend the Terminal type to include our custom properties
-type ExtendedTerminal = vscode.Terminal & {
- shellIntegration?: {
- cwd?: vscode.Uri
- executeCommand?: (command: string) => {
- read: () => AsyncIterable
- }
- }
+export interface ExitCodeDetails {
+ exitCode: number | undefined
+ signal?: number | undefined
+ signalName?: string
+ coreDumpPossible?: boolean
}
export class TerminalManager {
@@ -95,18 +106,156 @@ export class TerminalManager {
private processes: Map = new Map()
private disposables: vscode.Disposable[] = []
+ private interpretExitCode(exitCode: number | undefined): ExitCodeDetails {
+ if (exitCode === undefined) {
+ return { exitCode }
+ }
+
+ if (exitCode <= 128) {
+ return { exitCode }
+ }
+
+ const signal = exitCode - 128
+ const signals: Record = {
+ // Standard signals
+ 1: "SIGHUP",
+ 2: "SIGINT",
+ 3: "SIGQUIT",
+ 4: "SIGILL",
+ 5: "SIGTRAP",
+ 6: "SIGABRT",
+ 7: "SIGBUS",
+ 8: "SIGFPE",
+ 9: "SIGKILL",
+ 10: "SIGUSR1",
+ 11: "SIGSEGV",
+ 12: "SIGUSR2",
+ 13: "SIGPIPE",
+ 14: "SIGALRM",
+ 15: "SIGTERM",
+ 16: "SIGSTKFLT",
+ 17: "SIGCHLD",
+ 18: "SIGCONT",
+ 19: "SIGSTOP",
+ 20: "SIGTSTP",
+ 21: "SIGTTIN",
+ 22: "SIGTTOU",
+ 23: "SIGURG",
+ 24: "SIGXCPU",
+ 25: "SIGXFSZ",
+ 26: "SIGVTALRM",
+ 27: "SIGPROF",
+ 28: "SIGWINCH",
+ 29: "SIGIO",
+ 30: "SIGPWR",
+ 31: "SIGSYS",
+
+ // Real-time signals base
+ 34: "SIGRTMIN",
+
+ // SIGRTMIN+n signals
+ 35: "SIGRTMIN+1",
+ 36: "SIGRTMIN+2",
+ 37: "SIGRTMIN+3",
+ 38: "SIGRTMIN+4",
+ 39: "SIGRTMIN+5",
+ 40: "SIGRTMIN+6",
+ 41: "SIGRTMIN+7",
+ 42: "SIGRTMIN+8",
+ 43: "SIGRTMIN+9",
+ 44: "SIGRTMIN+10",
+ 45: "SIGRTMIN+11",
+ 46: "SIGRTMIN+12",
+ 47: "SIGRTMIN+13",
+ 48: "SIGRTMIN+14",
+ 49: "SIGRTMIN+15",
+
+ // SIGRTMAX-n signals
+ 50: "SIGRTMAX-14",
+ 51: "SIGRTMAX-13",
+ 52: "SIGRTMAX-12",
+ 53: "SIGRTMAX-11",
+ 54: "SIGRTMAX-10",
+ 55: "SIGRTMAX-9",
+ 56: "SIGRTMAX-8",
+ 57: "SIGRTMAX-7",
+ 58: "SIGRTMAX-6",
+ 59: "SIGRTMAX-5",
+ 60: "SIGRTMAX-4",
+ 61: "SIGRTMAX-3",
+ 62: "SIGRTMAX-2",
+ 63: "SIGRTMAX-1",
+ 64: "SIGRTMAX",
+ }
+
+ // These signals may produce core dumps:
+ // SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV
+ const coreDumpPossible = new Set([3, 4, 6, 7, 8, 11])
+
+ return {
+ exitCode,
+ signal,
+ signalName: signals[signal] || `Unknown Signal (${signal})`,
+ coreDumpPossible: coreDumpPossible.has(signal),
+ }
+ }
+
constructor() {
- let disposable: vscode.Disposable | undefined
+ let startDisposable: vscode.Disposable | undefined
+ let endDisposable: vscode.Disposable | undefined
try {
- disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
- // Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
- e?.execution?.read()
+ // onDidStartTerminalShellExecution
+ startDisposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
+ // Get a handle to the stream as early as possible:
+ const stream = e?.execution.read()
+ const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(e.terminal)
+ if (stream && terminalInfo) {
+ const process = this.processes.get(terminalInfo.id)
+ if (process) {
+ terminalInfo.stream = stream
+ terminalInfo.running = true
+ terminalInfo.streamClosed = false
+ process.emit("stream_available", terminalInfo.id, stream)
+ }
+ } else {
+ console.error("[TerminalManager] Stream failed, not registered for terminal")
+ }
+
+ console.info("[TerminalManager] Shell execution started:", {
+ hasExecution: !!e?.execution,
+ command: e?.execution?.commandLine?.value,
+ terminalId: terminalInfo?.id,
+ })
+ })
+
+ // onDidEndTerminalShellExecution
+ endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => {
+ const exitDetails = this.interpretExitCode(e?.exitCode)
+ console.info("[TerminalManager] Shell execution ended:", {
+ ...exitDetails,
+ })
+
+ // Signal completion to any waiting processes
+ for (const id of this.terminalIds) {
+ const info = TerminalRegistry.getTerminal(id)
+ if (info && info.terminal === e.terminal) {
+ info.running = false
+ const process = this.processes.get(id)
+ if (process) {
+ process.emit("shell_execution_complete", id, exitDetails)
+ }
+ break
+ }
+ }
})
} catch (error) {
- // console.error("Error setting up onDidEndTerminalShellExecution", error)
+ console.error("[TerminalManager] Error setting up shell execution handlers:", error)
}
- if (disposable) {
- this.disposables.push(disposable)
+ if (startDisposable) {
+ this.disposables.push(startDisposable)
+ }
+ if (endDisposable) {
+ this.disposables.push(endDisposable)
}
}
@@ -140,19 +289,16 @@ export class TerminalManager {
})
// if shell integration is already active, run the command immediately
- const terminal = terminalInfo.terminal as ExtendedTerminal
- if (terminal.shellIntegration) {
+ if (terminalInfo.terminal.shellIntegration) {
process.waitForShellIntegration = false
- process.run(terminal, command)
+ process.run(terminalInfo.terminal, command)
} else {
// docs recommend waiting 3s for shell integration to activate
- pWaitFor(() => (terminalInfo.terminal as ExtendedTerminal).shellIntegration !== undefined, {
- timeout: 4000,
- }).finally(() => {
+ pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
const existingProcess = this.processes.get(terminalInfo.id)
if (existingProcess && existingProcess.waitForShellIntegration) {
existingProcess.waitForShellIntegration = false
- existingProcess.run(terminal, command)
+ existingProcess.run(terminalInfo.terminal, command)
}
})
}
@@ -168,8 +314,7 @@ export class TerminalManager {
if (t.busy) {
return false
}
- const terminal = t.terminal as ExtendedTerminal
- const terminalCwd = terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
+ const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
if (!terminalCwd) {
return false
}
diff --git a/src/integrations/terminal/TerminalProcess.ts b/src/integrations/terminal/TerminalProcess.ts
index 5597350db3..99ef215e78 100644
--- a/src/integrations/terminal/TerminalProcess.ts
+++ b/src/integrations/terminal/TerminalProcess.ts
@@ -1,13 +1,24 @@
import { EventEmitter } from "events"
import stripAnsi from "strip-ansi"
import * as vscode from "vscode"
+import { inspect } from "util"
+
+import { ExitCodeDetails } from "./TerminalManager"
+import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
export interface TerminalProcessEvents {
line: [line: string]
continue: []
- completed: []
+ completed: [output?: string]
error: [error: Error]
no_shell_integration: []
+ /**
+ * Emitted when a shell execution completes
+ * @param id The terminal ID
+ * @param exitDetails Contains exit code and signal information if process was terminated by signal
+ */
+ shell_execution_complete: [id: number, exitDetails: ExitCodeDetails]
+ stream_available: [id: number, stream: AsyncIterable]
}
// how long to wait after a process outputs anything before we consider it "cool" again
@@ -17,104 +28,99 @@ const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
export class TerminalProcess extends EventEmitter {
waitForShellIntegration: boolean = true
private isListening: boolean = true
- private buffer: string = ""
+ private terminalInfo: TerminalInfo | undefined
+ private lastEmitTime_ms: number = 0
private fullOutput: string = ""
private lastRetrievedIndex: number = 0
isHot: boolean = false
private hotTimer: NodeJS.Timeout | null = null
- // constructor() {
- // super()
-
async run(terminal: vscode.Terminal, command: string) {
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
- const execution = terminal.shellIntegration.executeCommand(command)
- const stream = execution.read()
- // todo: need to handle errors
- let isFirstChunk = true
- let didOutputNonCommand = false
- let didEmitEmptyLine = false
+ // Get terminal info to access stream
+ const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(terminal)
+ if (!terminalInfo) {
+ console.error("[TerminalProcess] Terminal not found in registry")
+ this.emit("no_shell_integration")
+ this.emit("completed")
+ this.emit("continue")
+ return
+ }
+
+ // When executeCommand() is called, onDidStartTerminalShellExecution will fire in TerminalManager
+ // which creates a new stream via execution.read() and emits 'stream_available'
+ const streamAvailable = new Promise>((resolve) => {
+ this.once("stream_available", (id: number, stream: AsyncIterable) => {
+ if (id === terminalInfo.id) {
+ resolve(stream)
+ }
+ })
+ })
+
+ // Create promise that resolves when shell execution completes for this terminal
+ const shellExecutionComplete = new Promise((resolve) => {
+ this.once("shell_execution_complete", (id: number, exitDetails: ExitCodeDetails) => {
+ if (id === terminalInfo.id) {
+ resolve(exitDetails)
+ }
+ })
+ })
+
+ // getUnretrievedOutput needs to know if streamClosed, so store this for later
+ this.terminalInfo = terminalInfo
+
+ // Execute command
+ terminal.shellIntegration.executeCommand(command)
+ this.isHot = true
+
+ // Wait for stream to be available
+ const stream = await streamAvailable
+
+ let preOutput = ""
+ let commandOutputStarted = false
+
+ /*
+ * Extract clean output from raw accumulated output. FYI:
+ * ]633 is a custom sequence number used by VSCode shell integration:
+ * - OSC 633 ; A ST - Mark prompt start
+ * - OSC 633 ; B ST - Mark prompt end
+ * - OSC 633 ; C ST - Mark pre-execution (start of command output)
+ * - OSC 633 ; D [; ] ST - Mark execution finished with optional exit code
+ * - OSC 633 ; E ; [; ] ST - Explicitly set command line with optional nonce
+ */
+
+ // Process stream data
for await (let data of stream) {
- // 1. Process chunk and remove artifacts
- if (isFirstChunk) {
- /*
- The first chunk we get from this stream needs to be processed to be more human readable, ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc.
- */
-
- // bug where sometimes the command output makes its way into vscode shell integration metadata
- /*
- ]633 is a custom sequence number used by VSCode shell integration:
- - OSC 633 ; A ST - Mark prompt start
- - OSC 633 ; B ST - Mark prompt end
- - OSC 633 ; C ST - Mark pre-execution (start of command output)
- - OSC 633 ; D [; ] ST - Mark execution finished with optional exit code
- - OSC 633 ; E ; [; ] ST - Explicitly set command line with optional nonce
- */
- // if you print this data you might see something like "eecho hello worldo hello world;5ba85d14-e92a-40c4-b2fd-71525581eeb0]633;C" but this is actually just a bunch of escape sequences, ignore up to the first ;C
- /* ddateb15026-6a64-40db-b21f-2a621a9830f0]633;CTue Sep 17 06:37:04 EDT 2024 % ]633;D;0]633;P;Cwd=/Users/saoud/Repositories/test */
- // Gets output between ]633;C (command start) and ]633;D (command end)
- const outputBetweenSequences = this.removeLastLineArtifacts(
- data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "",
- ).trim()
-
- // Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
- // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
- const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
- const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
- if (lastMatch && lastMatch.index !== undefined) {
- data = data.slice(lastMatch.index + lastMatch[0].length)
+ // Check for command output start marker
+ if (!commandOutputStarted) {
+ preOutput += data
+ const match = this.matchAfterVsceStartMarkers(data)
+ if (match !== undefined) {
+ commandOutputStarted = true
+ data = match
+ this.fullOutput = "" // Reset fullOutput when command actually starts
+ } else {
+ continue
}
- // Place output back after removing vscode sequences
- if (outputBetweenSequences) {
- data = outputBetweenSequences + "\n" + data
- }
- // remove ansi
- data = stripAnsi(data)
- // Split data by newlines
- let lines = data ? data.split("\n") : []
- // Remove non-human readable characters from the first line
- if (lines.length > 0) {
- lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
- }
- // Check if first two characters are the same, if so remove the first character
- if (lines.length > 0 && lines[0].length >= 2 && lines[0][0] === lines[0][1]) {
- lines[0] = lines[0].slice(1)
- }
- // Remove everything up to the first alphanumeric character for first two lines
- if (lines.length > 0) {
- lines[0] = lines[0].replace(/^[^a-zA-Z0-9]*/, "")
- }
- if (lines.length > 1) {
- lines[1] = lines[1].replace(/^[^a-zA-Z0-9]*/, "")
- }
- // Join lines back
- data = lines.join("\n")
- isFirstChunk = false
- } else {
- data = stripAnsi(data)
}
- // first few chunks could be the command being echoed back, so we must ignore
- // note this means that 'echo' commands wont work
- if (!didOutputNonCommand) {
- const lines = data.split("\n")
- for (let i = 0; i < lines.length; i++) {
- if (command.includes(lines[i].trim())) {
- lines.splice(i, 1)
- i-- // Adjust index after removal
- } else {
- didOutputNonCommand = true
- break
- }
- }
- data = lines.join("\n")
+ // Command output started, accumulate data without filtering.
+ // notice to future programmers: do not add escape sequence
+ // filtering here: fullOutput cannot change in length (see getUnretrievedOutput),
+ // and chunks may not be complete so you cannot rely on detecting or removing escape sequences mid-stream.
+ this.fullOutput += data
+
+ // For non-immediately returning commands we want to show loading spinner
+ // right away but this wouldnt happen until it emits a line break, so
+ // as soon as we get any output we emit to let webview know to show spinner
+ const now = Date.now()
+ if (this.isListening && (now - this.lastEmitTime_ms > 100 || this.lastEmitTime_ms === 0)) {
+ this.emitRemainingBufferIfListening()
+ this.lastEmitTime_ms = now
}
- // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
- data = data.replace(/,/g, "")
-
- // 2. Set isHot depending on the command
- // Set to hot to stall API requests until terminal is cool again
+ // 2. Set isHot depending on the command.
+ // This stalls API requests until terminal is cool again.
this.isHot = true
if (this.hotTimer) {
clearTimeout(this.hotTimer)
@@ -144,21 +150,37 @@ export class TerminalProcess extends EventEmitter {
},
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
)
-
- // For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
- if (!didEmitEmptyLine && !this.fullOutput && data) {
- this.emit("line", "") // empty line to indicate start of command output stream
- didEmitEmptyLine = true
- }
-
- this.fullOutput += data
- if (this.isListening) {
- this.emitIfEol(data)
- this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
- }
}
- this.emitRemainingBufferIfListening()
+ // Set streamClosed immediately after stream ends
+ if (this.terminalInfo) {
+ this.terminalInfo.streamClosed = true
+ }
+
+ // Wait for shell execution to complete and handle exit details
+ const exitDetails = await shellExecutionComplete
+ this.isHot = false
+
+ if (commandOutputStarted) {
+ // Emit any remaining output before completing
+ this.emitRemainingBufferIfListening()
+ } else {
+ console.error(
+ "[Terminal Process] VSCE output start escape sequence (]633;C or ]133;C) not received! VSCE Bug? preOutput: " +
+ inspect(preOutput, { colors: false, breakLength: Infinity }),
+ )
+ }
+
+ // console.debug("[Terminal Process] raw output: " + inspect(output, { colors: false, breakLength: Infinity }))
+
+ // fullOutput begins after C marker so we only need to trim off D marker
+ // (if D exists, see VSCode bug# 237208):
+ const match = this.matchBeforeVsceEndMarkers(this.fullOutput)
+ if (match !== undefined) {
+ this.fullOutput = match
+ }
+
+ // console.debug(`[Terminal Process] processed output via ${matchSource}: ` + inspect(output, { colors: false, breakLength: Infinity }))
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
if (this.hotTimer) {
@@ -166,7 +188,7 @@ export class TerminalProcess extends EventEmitter {
}
this.isHot = false
- this.emit("completed")
+ this.emit("completed", this.removeEscapeSequences(this.fullOutput))
this.emit("continue")
} else {
terminal.sendText(command, true)
@@ -182,29 +204,12 @@ export class TerminalProcess extends EventEmitter {
}
}
- // Inspired by https://github.com/sindresorhus/execa/blob/main/lib/transform/split.js
- private emitIfEol(chunk: string) {
- this.buffer += chunk
- let lineEndIndex: number
- while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
- let line = this.buffer.slice(0, lineEndIndex).trimEnd() // removes trailing \r
- // Remove \r if present (for Windows-style line endings)
- // if (line.endsWith("\r")) {
- // line = line.slice(0, -1)
- // }
- this.emit("line", line)
- this.buffer = this.buffer.slice(lineEndIndex + 1)
- }
- }
-
private emitRemainingBufferIfListening() {
- if (this.buffer && this.isListening) {
- const remainingBuffer = this.removeLastLineArtifacts(this.buffer)
- if (remainingBuffer) {
+ if (this.isListening) {
+ const remainingBuffer = this.getUnretrievedOutput()
+ if (remainingBuffer !== "") {
this.emit("line", remainingBuffer)
}
- this.buffer = ""
- this.lastRetrievedIndex = this.fullOutput.length
}
}
@@ -215,22 +220,180 @@ export class TerminalProcess extends EventEmitter {
this.emit("continue")
}
+ // Returns complete lines with their carriage returns.
+ // The final line may lack a carriage return if the program didn't send one.
getUnretrievedOutput(): string {
- const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
- this.lastRetrievedIndex = this.fullOutput.length
- return this.removeLastLineArtifacts(unretrieved)
+ // Get raw unretrieved output
+ let outputToProcess = this.fullOutput.slice(this.lastRetrievedIndex)
+
+ // Check for VSCE command end markers
+ const index633 = outputToProcess.indexOf("\x1b]633;D")
+ const index133 = outputToProcess.indexOf("\x1b]133;D")
+ let endIndex = -1
+
+ if (index633 !== -1 && index133 !== -1) {
+ endIndex = Math.min(index633, index133)
+ } else if (index633 !== -1) {
+ endIndex = index633
+ } else if (index133 !== -1) {
+ endIndex = index133
+ }
+
+ // If no end markers were found yet (possibly due to VSCode bug#237208):
+ // For active streams: return only complete lines (up to last \n).
+ // For closed streams: return all remaining content.
+ if (endIndex === -1) {
+ if (!this.terminalInfo?.streamClosed) {
+ // Stream still running - only process complete lines
+ endIndex = outputToProcess.lastIndexOf("\n")
+ if (endIndex === -1) {
+ // No complete lines
+ return ""
+ }
+
+ // Include carriage return
+ endIndex++
+ } else {
+ // Stream closed - process all remaining output
+ endIndex = outputToProcess.length
+ }
+ }
+
+ // Update index and slice output
+ this.lastRetrievedIndex += endIndex
+ outputToProcess = outputToProcess.slice(0, endIndex)
+
+ // Clean and return output
+ return this.removeEscapeSequences(outputToProcess)
}
- // some processing to remove artifacts like '%' at the end of the buffer (it seems that since vsode uses % at the beginning of newlines in terminal, it makes its way into the stream)
- // This modification will remove '%', '$', '#', or '>' followed by optional whitespace
- removeLastLineArtifacts(output: string) {
- const lines = output.trimEnd().split("\n")
- if (lines.length > 0) {
- const lastLine = lines[lines.length - 1]
- // Remove prompt characters and trailing whitespace from the last line
- lines[lines.length - 1] = lastLine.replace(/[%$#>]\s*$/, "")
+ private stringIndexMatch(
+ data: string,
+ prefix?: string,
+ suffix?: string,
+ bell: string = "\x07",
+ ): string | undefined {
+ let startIndex: number
+ let endIndex: number
+ let prefixLength: number
+
+ if (prefix === undefined) {
+ startIndex = 0
+ prefixLength = 0
+ } else {
+ startIndex = data.indexOf(prefix)
+ if (startIndex === -1) {
+ return undefined
+ }
+ if (bell.length > 0) {
+ // Find the bell character after the prefix
+ const bellIndex = data.indexOf(bell, startIndex + prefix.length)
+ if (bellIndex === -1) {
+ return undefined
+ }
+
+ const distanceToBell = bellIndex - startIndex
+
+ prefixLength = distanceToBell + bell.length
+ } else {
+ prefixLength = prefix.length
+ }
}
- return lines.join("\n").trimEnd()
+
+ const contentStart = startIndex + prefixLength
+
+ if (suffix === undefined) {
+ // When suffix is undefined, match to end
+ endIndex = data.length
+ } else {
+ endIndex = data.indexOf(suffix, contentStart)
+ if (endIndex === -1) {
+ return undefined
+ }
+ }
+
+ return data.slice(contentStart, endIndex)
+ }
+
+ // Removes ANSI escape sequences and VSCode-specific terminal control codes from output.
+ // While stripAnsi handles most ANSI codes, VSCode's shell integration adds custom
+ // escape sequences (OSC 633) that need special handling. These sequences control
+ // terminal features like marking command start/end and setting prompts.
+ //
+ // This method could be extended to handle other escape sequences, but any additions
+ // should be carefully considered to ensure they only remove control codes and don't
+ // alter the actual content or behavior of the output stream.
+ private removeEscapeSequences(str: string): string {
+ return stripAnsi(str.replace(/\x1b\]633;[^\x07]+\x07/gs, "").replace(/\x1b\]133;[^\x07]+\x07/gs, ""))
+ }
+
+ /**
+ * Helper function to match VSCode shell integration start markers (C).
+ * Looks for content after ]633;C or ]133;C markers.
+ * If both exist, takes the content after the last marker found.
+ */
+ private matchAfterVsceStartMarkers(data: string): string | undefined {
+ return this.matchVsceMarkers(data, "\x1b]633;C", "\x1b]133;C", undefined, undefined)
+ }
+
+ /**
+ * Helper function to match VSCode shell integration end markers (D).
+ * Looks for content before ]633;D or ]133;D markers.
+ * If both exist, takes the content before the first marker found.
+ */
+ private matchBeforeVsceEndMarkers(data: string): string | undefined {
+ return this.matchVsceMarkers(data, undefined, undefined, "\x1b]633;D", "\x1b]133;D")
+ }
+
+ /**
+ * Handles VSCode shell integration markers for command output:
+ *
+ * For C (Command Start):
+ * - Looks for content after ]633;C or ]133;C markers
+ * - These markers indicate the start of command output
+ * - If both exist, takes the content after the last marker found
+ * - This ensures we get the actual command output after any shell integration prefixes
+ *
+ * For D (Command End):
+ * - Looks for content before ]633;D or ]133;D markers
+ * - These markers indicate command completion
+ * - If both exist, takes the content before the first marker found
+ * - This ensures we don't include shell integration suffixes in the output
+ *
+ * In both cases, checks 633 first since it's more commonly used in VSCode shell integration
+ *
+ * @param data The string to search for markers in
+ * @param prefix633 The 633 marker to match after (for C markers)
+ * @param prefix133 The 133 marker to match after (for C markers)
+ * @param suffix633 The 633 marker to match before (for D markers)
+ * @param suffix133 The 133 marker to match before (for D markers)
+ * @returns The content between/after markers, or undefined if no markers found
+ *
+ * Note: Always makes exactly 2 calls to stringIndexMatch regardless of match results.
+ * Using string indexOf matching is ~500x faster than regular expressions, so even
+ * matching twice is still very efficient comparatively.
+ */
+ private matchVsceMarkers(
+ data: string,
+ prefix633: string | undefined,
+ prefix133: string | undefined,
+ suffix633: string | undefined,
+ suffix133: string | undefined,
+ ): string | undefined {
+ // Support both VSCode shell integration markers (633 and 133)
+ // Check 633 first since it's more commonly used in VSCode shell integration
+ let match133: string | undefined
+ const match633 = this.stringIndexMatch(data, prefix633, suffix633)
+
+ // Must check explicitly for undefined because stringIndexMatch can return empty strings
+ // that are valid matches (e.g., when a marker exists but has no content between markers)
+ if (match633 !== undefined) {
+ match133 = this.stringIndexMatch(match633, prefix133, suffix133)
+ } else {
+ match133 = this.stringIndexMatch(data, prefix133, suffix133)
+ }
+
+ return match133 !== undefined ? match133 : match633
}
}
diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts
index 2fb49e4825..69a21d94fd 100644
--- a/src/integrations/terminal/TerminalRegistry.ts
+++ b/src/integrations/terminal/TerminalRegistry.ts
@@ -5,6 +5,9 @@ export interface TerminalInfo {
busy: boolean
lastCommand: string
id: number
+ stream?: AsyncIterable
+ running: boolean
+ streamClosed: boolean
}
// Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output).
@@ -20,34 +23,61 @@ export class TerminalRegistry {
iconPath: new vscode.ThemeIcon("rocket"),
env: {
PAGER: "cat",
+
+ // VSCode bug#237208: Command output can be lost due to a race between completion
+ // sequences and consumers. Add 50ms delay via PROMPT_COMMAND to ensure the
+ // \x1b]633;D escape sequence arrives after command output is processed.
+ PROMPT_COMMAND: "sleep 0.050",
+
+ // VTE must be disabled because it prevents the prompt command above from executing
+ // See https://wiki.gnome.org/Apps/Terminal/VTE
+ VTE_VERSION: "0",
},
})
+
const newInfo: TerminalInfo = {
terminal,
busy: false,
lastCommand: "",
id: this.nextTerminalId++,
+ running: false,
+ streamClosed: false,
}
+
this.terminals.push(newInfo)
return newInfo
}
static getTerminal(id: number): TerminalInfo | undefined {
const terminalInfo = this.terminals.find((t) => t.id === id)
+
if (terminalInfo && this.isTerminalClosed(terminalInfo.terminal)) {
this.removeTerminal(id)
return undefined
}
+
return terminalInfo
}
static updateTerminal(id: number, updates: Partial) {
const terminal = this.getTerminal(id)
+
if (terminal) {
Object.assign(terminal, updates)
}
}
+ static getTerminalInfoByTerminal(terminal: vscode.Terminal): TerminalInfo | undefined {
+ const terminalInfo = this.terminals.find((t) => t.terminal === terminal)
+
+ if (terminalInfo && this.isTerminalClosed(terminalInfo.terminal)) {
+ this.removeTerminal(terminalInfo.id)
+ return undefined
+ }
+
+ return terminalInfo
+ }
+
static removeTerminal(id: number) {
this.terminals = this.terminals.filter((t) => t.id !== id)
}
diff --git a/src/integrations/terminal/__tests__/TerminalProcess.test.ts b/src/integrations/terminal/__tests__/TerminalProcess.test.ts
index 9ccbaef920..11c0339f27 100644
--- a/src/integrations/terminal/__tests__/TerminalProcess.test.ts
+++ b/src/integrations/terminal/__tests__/TerminalProcess.test.ts
@@ -1,9 +1,24 @@
-import { TerminalProcess, mergePromise } from "../TerminalProcess"
-import * as vscode from "vscode"
-import { EventEmitter } from "events"
+// npx jest src/integrations/terminal/__tests__/TerminalProcess.test.ts
-// Mock vscode
-jest.mock("vscode")
+import * as vscode from "vscode"
+
+import { TerminalProcess, mergePromise } from "../TerminalProcess"
+import { TerminalInfo, TerminalRegistry } from "../TerminalRegistry"
+
+// Mock vscode.window.createTerminal
+const mockCreateTerminal = jest.fn()
+
+jest.mock("vscode", () => ({
+ window: {
+ createTerminal: (...args: any[]) => {
+ mockCreateTerminal(...args)
+ return {
+ exitStatus: undefined,
+ }
+ },
+ },
+ ThemeIcon: jest.fn(),
+}))
describe("TerminalProcess", () => {
let terminalProcess: TerminalProcess
@@ -14,6 +29,7 @@ describe("TerminalProcess", () => {
}
}
>
+ let mockTerminalInfo: TerminalInfo
let mockExecution: any
let mockStream: AsyncIterableIterator
@@ -25,7 +41,7 @@ describe("TerminalProcess", () => {
shellIntegration: {
executeCommand: jest.fn(),
},
- name: "Mock Terminal",
+ name: "Roo Code",
processId: Promise.resolve(123),
creationOptions: {},
exitStatus: undefined,
@@ -42,27 +58,39 @@ describe("TerminalProcess", () => {
}
>
+ mockTerminalInfo = {
+ terminal: mockTerminal,
+ busy: false,
+ lastCommand: "",
+ id: 1,
+ running: false,
+ streamClosed: false,
+ }
+
+ TerminalRegistry["terminals"].push(mockTerminalInfo)
+
// Reset event listeners
terminalProcess.removeAllListeners()
})
describe("run", () => {
it("handles shell integration commands correctly", async () => {
- const lines: string[] = []
- terminalProcess.on("line", (line) => {
- // Skip empty lines used for loading spinner
- if (line !== "") {
- lines.push(line)
+ let lines: string[] = []
+
+ terminalProcess.on("completed", (output) => {
+ if (output) {
+ lines = output.split("\n")
}
})
- // Mock stream data with shell integration sequences
+ // Mock stream data with shell integration sequences.
mockStream = (async function* () {
- // The first chunk contains the command start sequence
+ yield "\x1b]633;C\x07" // The first chunk contains the command start sequence with bell character.
yield "Initial output\n"
yield "More output\n"
- // The last chunk contains the command end sequence
yield "Final output"
+ yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character.
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
})()
mockExecution = {
@@ -71,12 +99,9 @@ describe("TerminalProcess", () => {
mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
- const completedPromise = new Promise((resolve) => {
- terminalProcess.once("completed", resolve)
- })
-
- await terminalProcess.run(mockTerminal, "test command")
- await completedPromise
+ const runPromise = terminalProcess.run(mockTerminal, "test command")
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
+ await runPromise
expect(lines).toEqual(["Initial output", "More output", "Final output"])
expect(terminalProcess.isHot).toBe(false)
@@ -99,95 +124,41 @@ describe("TerminalProcess", () => {
})
it("sets hot state for compiling commands", async () => {
- const lines: string[] = []
- terminalProcess.on("line", (line) => {
- if (line !== "") {
- lines.push(line)
+ let lines: string[] = []
+
+ terminalProcess.on("completed", (output) => {
+ if (output) {
+ lines = output.split("\n")
}
})
- // Create a promise that resolves when the first chunk is processed
- const firstChunkProcessed = new Promise((resolve) => {
- terminalProcess.on("line", () => resolve())
+ const completePromise = new Promise((resolve) => {
+ terminalProcess.on("shell_execution_complete", () => resolve())
})
mockStream = (async function* () {
+ yield "\x1b]633;C\x07" // The first chunk contains the command start sequence with bell character.
yield "compiling...\n"
- // Wait to ensure hot state check happens after first chunk
- await new Promise((resolve) => setTimeout(resolve, 10))
yield "still compiling...\n"
yield "done"
+ yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character.
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
})()
- mockExecution = {
+ mockTerminal.shellIntegration.executeCommand.mockReturnValue({
read: jest.fn().mockReturnValue(mockStream),
- }
-
- mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
-
- // Start the command execution
- const runPromise = terminalProcess.run(mockTerminal, "npm run build")
-
- // Wait for the first chunk to be processed
- await firstChunkProcessed
-
- // Hot state should be true while compiling
- expect(terminalProcess.isHot).toBe(true)
-
- // Complete the execution
- const completedPromise = new Promise((resolve) => {
- terminalProcess.once("completed", resolve)
})
+ const runPromise = terminalProcess.run(mockTerminal, "npm run build")
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
+
+ expect(terminalProcess.isHot).toBe(true)
await runPromise
- await completedPromise
expect(lines).toEqual(["compiling...", "still compiling...", "done"])
- })
- })
- describe("buffer processing", () => {
- it("correctly processes and emits lines", () => {
- const lines: string[] = []
- terminalProcess.on("line", (line) => lines.push(line))
-
- // Simulate incoming chunks
- terminalProcess["emitIfEol"]("first line\n")
- terminalProcess["emitIfEol"]("second")
- terminalProcess["emitIfEol"](" line\n")
- terminalProcess["emitIfEol"]("third line")
-
- expect(lines).toEqual(["first line", "second line"])
-
- // Process remaining buffer
- terminalProcess["emitRemainingBufferIfListening"]()
- expect(lines).toEqual(["first line", "second line", "third line"])
- })
-
- it("handles Windows-style line endings", () => {
- const lines: string[] = []
- terminalProcess.on("line", (line) => lines.push(line))
-
- terminalProcess["emitIfEol"]("line1\r\nline2\r\n")
-
- expect(lines).toEqual(["line1", "line2"])
- })
- })
-
- describe("removeLastLineArtifacts", () => {
- it("removes terminal artifacts from output", () => {
- const cases = [
- ["output%", "output"],
- ["output$ ", "output"],
- ["output#", "output"],
- ["output> ", "output"],
- ["multi\nline%", "multi\nline"],
- ["no artifacts", "no artifacts"],
- ]
-
- for (const [input, expected] of cases) {
- expect(terminalProcess["removeLastLineArtifacts"](input)).toBe(expected)
- }
+ await completePromise
+ expect(terminalProcess.isHot).toBe(false)
})
})
@@ -204,14 +175,14 @@ describe("TerminalProcess", () => {
})
describe("getUnretrievedOutput", () => {
- it("returns and clears unretrieved output", () => {
- terminalProcess["fullOutput"] = "previous\nnew output"
- terminalProcess["lastRetrievedIndex"] = 9 // After "previous\n"
+ it.only("returns and clears unretrieved output", () => {
+ terminalProcess["fullOutput"] = `\x1b]633;C\x07previous\nnew output\x1b]633;D\x07`
+ terminalProcess["lastRetrievedIndex"] = 17 // After "previous\n"
const unretrieved = terminalProcess.getUnretrievedOutput()
-
expect(unretrieved).toBe("new output")
- expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length)
+
+ expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length - "previous".length)
})
})
diff --git a/src/integrations/terminal/__tests__/TerminalRegistry.test.ts b/src/integrations/terminal/__tests__/TerminalRegistry.test.ts
index cc667a851b..a2b8fcd3b0 100644
--- a/src/integrations/terminal/__tests__/TerminalRegistry.test.ts
+++ b/src/integrations/terminal/__tests__/TerminalRegistry.test.ts
@@ -1,4 +1,5 @@
-import * as vscode from "vscode"
+// npx jest src/integrations/terminal/__tests__/TerminalRegistry.test.ts
+
import { TerminalRegistry } from "../TerminalRegistry"
// Mock vscode.window.createTerminal
@@ -30,6 +31,8 @@ describe("TerminalRegistry", () => {
iconPath: expect.any(Object),
env: {
PAGER: "cat",
+ PROMPT_COMMAND: "sleep 0.050",
+ VTE_VERSION: "0",
},
})
})
diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts
index 31fe219f04..cbd674fc07 100644
--- a/src/shared/combineCommandSequences.ts
+++ b/src/shared/combineCommandSequences.ts
@@ -44,7 +44,7 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
// handle cases where we receive empty command_output (ie when extension is relinquishing control over exit command button)
const output = messages[j].text || ""
if (output.length > 0) {
- combinedText += "\n" + output
+ combinedText += output
}
}
j++
From e49d886d63965063b4392f8c36f1c052d7ac134c Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 10:21:52 -0800
Subject: [PATCH 094/298] Re-enable tests
---
src/integrations/terminal/__tests__/TerminalProcess.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/integrations/terminal/__tests__/TerminalProcess.test.ts b/src/integrations/terminal/__tests__/TerminalProcess.test.ts
index 11c0339f27..44cae92580 100644
--- a/src/integrations/terminal/__tests__/TerminalProcess.test.ts
+++ b/src/integrations/terminal/__tests__/TerminalProcess.test.ts
@@ -175,7 +175,7 @@ describe("TerminalProcess", () => {
})
describe("getUnretrievedOutput", () => {
- it.only("returns and clears unretrieved output", () => {
+ it("returns and clears unretrieved output", () => {
terminalProcess["fullOutput"] = `\x1b]633;C\x07previous\nnew output\x1b]633;D\x07`
terminalProcess["lastRetrievedIndex"] = 17 // After "previous\n"
From 4570ead71fb0ffead4af6ae3467831f0ed01f2de Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Fri, 28 Feb 2025 17:17:19 -0500
Subject: [PATCH 095/298] Update ChatView.tsx
---
webview-ui/src/components/chat/ChatView.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index fcd1ba9a3b..786f1ea957 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -880,7 +880,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const placeholderText = useMemo(() => {
const baseText = task ? "Type a message..." : "Type your task here..."
const contextText = "(@ to add context, / to switch modes"
- const imageText = shouldDisableImages ? "hold shift to drag in files" : ", hold shift to drag in files/images"
+ const imageText = shouldDisableImages ? ", hold shift to drag in files" : ", hold shift to drag in files/images"
return baseText + `\n${contextText}${imageText})`
}, [task, shouldDisableImages])
From 4caa49228fcfdac74872cb6d0486e380d9c984e6 Mon Sep 17 00:00:00 2001
From: ashktn
Date: Fri, 28 Feb 2025 18:54:34 -0500
Subject: [PATCH 096/298] feat: Add support for Gemini models on Vertex AI
---
.changeset/dry-suits-shake.md | 5 +
package-lock.json | 13 +
package.json | 1 +
src/api/providers/__tests__/vertex.test.ts | 334 +++++++++++++++--
src/api/providers/vertex.ts | 128 ++++++-
.../__tests__/vertex-gemini-format.test.ts | 338 ++++++++++++++++++
src/api/transform/vertex-gemini-format.ts | 83 +++++
src/shared/api.ts | 40 +++
8 files changed, 897 insertions(+), 45 deletions(-)
create mode 100644 .changeset/dry-suits-shake.md
create mode 100644 src/api/transform/__tests__/vertex-gemini-format.test.ts
create mode 100644 src/api/transform/vertex-gemini-format.ts
diff --git a/.changeset/dry-suits-shake.md b/.changeset/dry-suits-shake.md
new file mode 100644
index 0000000000..95bb8d3db4
--- /dev/null
+++ b/.changeset/dry-suits-shake.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": minor
+---
+
+Add Gemini models on Vertex AI
diff --git a/package-lock.json b/package-lock.json
index 950769b39b..17cad607a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
+ "@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.0.1",
@@ -3241,6 +3242,18 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "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",
diff --git a/package.json b/package.json
index fe64af16d6..8e8bb0712a 100644
--- a/package.json
+++ b/package.json
@@ -308,6 +308,7 @@
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0",
+ "@google-cloud/vertexai": "^1.9.3",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index 9cf92f0a16..d3f34fcfa1 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -6,6 +6,7 @@ import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { VertexHandler } from "../vertex"
import { ApiStreamChunk } from "../../transform/stream"
+import { VertexAI } from "@google-cloud/vertexai"
// Mock Vertex SDK
jest.mock("@anthropic-ai/vertex-sdk", () => ({
@@ -49,24 +50,100 @@ jest.mock("@anthropic-ai/vertex-sdk", () => ({
})),
}))
+// Mock Vertex Gemini SDK
+jest.mock("@google-cloud/vertexai", () => {
+ const mockGenerateContentStream = jest.fn().mockImplementation(() => {
+ return {
+ stream: {
+ async *[Symbol.asyncIterator]() {
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: "Test Gemini response" }],
+ },
+ },
+ ],
+ }
+ },
+ },
+ response: {
+ usageMetadata: {
+ promptTokenCount: 5,
+ candidatesTokenCount: 10,
+ },
+ },
+ }
+ })
+
+ const mockGenerateContent = jest.fn().mockResolvedValue({
+ response: {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: "Test Gemini response" }],
+ },
+ },
+ ],
+ },
+ })
+
+ const mockGenerativeModel = jest.fn().mockImplementation(() => {
+ return {
+ generateContentStream: mockGenerateContentStream,
+ generateContent: mockGenerateContent,
+ }
+ })
+
+ return {
+ VertexAI: jest.fn().mockImplementation(() => {
+ return {
+ getGenerativeModel: mockGenerativeModel,
+ }
+ }),
+ GenerativeModel: mockGenerativeModel,
+ }
+})
+
describe("VertexHandler", () => {
let handler: VertexHandler
- beforeEach(() => {
- handler = new VertexHandler({
- apiModelId: "claude-3-5-sonnet-v2@20241022",
- vertexProjectId: "test-project",
- vertexRegion: "us-central1",
- })
- })
-
describe("constructor", () => {
- it("should initialize with provided config", () => {
+ it("should initialize with provided config for Claude", () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
expect(AnthropicVertex).toHaveBeenCalledWith({
projectId: "test-project",
region: "us-central1",
})
})
+
+ it("should initialize with provided config for Gemini", () => {
+ handler = new VertexHandler({
+ apiModelId: "gemini-1.5-pro-001",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
+ expect(VertexAI).toHaveBeenCalledWith({
+ project: "test-project",
+ location: "us-central1",
+ })
+ })
+
+ it("should throw error for invalid model", () => {
+ expect(() => {
+ new VertexHandler({
+ apiModelId: "invalid-model",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+ }).toThrow("Unknown model ID: invalid-model")
+ })
})
describe("createMessage", () => {
@@ -83,7 +160,13 @@ describe("VertexHandler", () => {
const systemPrompt = "You are a helpful assistant"
- it("should handle streaming responses correctly", async () => {
+ it("should handle streaming responses correctly for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "message_start",
@@ -127,7 +210,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks: ApiStreamChunk[] = []
@@ -187,7 +270,58 @@ describe("VertexHandler", () => {
})
})
- it("should handle multiple content blocks with line breaks", async () => {
+ it("should handle streaming responses correctly for Gemini", async () => {
+ const mockGemini = require("@google-cloud/vertexai")
+ const mockGenerateContentStream = mockGemini.VertexAI().getGenerativeModel().generateContentStream
+ handler = new VertexHandler({
+ apiModelId: "gemini-1.5-pro-001",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
+ const stream = handler.createMessage(systemPrompt, mockMessages)
+ const chunks: ApiStreamChunk[] = []
+
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ expect(chunks.length).toBe(2)
+ expect(chunks[0]).toEqual({
+ type: "text",
+ text: "Test Gemini response",
+ })
+ expect(chunks[1]).toEqual({
+ type: "usage",
+ inputTokens: 5,
+ outputTokens: 10,
+ })
+
+ expect(mockGenerateContentStream).toHaveBeenCalledWith({
+ contents: [
+ {
+ role: "user",
+ parts: [{ text: "Hello" }],
+ },
+ {
+ role: "model",
+ parts: [{ text: "Hi there!" }],
+ },
+ ],
+ generationConfig: {
+ maxOutputTokens: 16384,
+ temperature: 0,
+ },
+ })
+ })
+
+ it("should handle multiple content blocks with line breaks for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "content_block_start",
@@ -216,7 +350,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks: ApiStreamChunk[] = []
@@ -240,10 +374,16 @@ describe("VertexHandler", () => {
})
})
- it("should handle API errors", async () => {
+ it("should handle API errors for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockError = new Error("Vertex API error")
const mockCreate = jest.fn().mockRejectedValue(mockError)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
@@ -254,7 +394,13 @@ describe("VertexHandler", () => {
}).rejects.toThrow("Vertex API error")
})
- it("should handle prompt caching for supported models", async () => {
+ it("should handle prompt caching for supported models for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "message_start",
@@ -299,7 +445,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, [
{
@@ -383,7 +529,13 @@ describe("VertexHandler", () => {
)
})
- it("should handle cache-related usage metrics", async () => {
+ it("should handle cache-related usage metrics for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "message_start",
@@ -415,7 +567,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks: ApiStreamChunk[] = []
@@ -442,7 +594,13 @@ describe("VertexHandler", () => {
const systemPrompt = "You are a helpful assistant"
- it("should handle thinking content blocks and deltas", async () => {
+ it("should handle thinking content blocks and deltas for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "message_start",
@@ -488,7 +646,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks: ApiStreamChunk[] = []
@@ -510,7 +668,13 @@ describe("VertexHandler", () => {
expect(textChunks[1].text).toBe("Here's my answer:")
})
- it("should handle multiple thinking blocks with line breaks", async () => {
+ it("should handle multiple thinking blocks with line breaks for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockStream = [
{
type: "content_block_start",
@@ -539,7 +703,7 @@ describe("VertexHandler", () => {
}
const mockCreate = jest.fn().mockResolvedValue(asyncIterator)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
const chunks: ApiStreamChunk[] = []
@@ -565,10 +729,16 @@ describe("VertexHandler", () => {
})
describe("completePrompt", () => {
- it("should complete prompt successfully", async () => {
+ it("should complete prompt successfully for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("Test response")
- expect(handler["client"].messages.create).toHaveBeenCalledWith({
+ expect(handler["anthropicClient"].messages.create).toHaveBeenCalledWith({
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
@@ -583,31 +753,109 @@ describe("VertexHandler", () => {
})
})
- it("should handle API errors", async () => {
+ it("should complete prompt successfully for Gemini", async () => {
+ const mockGemini = require("@google-cloud/vertexai")
+ const mockGenerateContent = mockGemini.VertexAI().getGenerativeModel().generateContent
+
+ handler = new VertexHandler({
+ apiModelId: "gemini-1.5-pro-001",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
+ const result = await handler.completePrompt("Test prompt")
+ expect(result).toBe("Test Gemini response")
+ expect(mockGenerateContent).toHaveBeenCalled()
+ expect(mockGenerateContent).toHaveBeenCalledWith({
+ contents: [{ role: "user", parts: [{ text: "Test prompt" }] }],
+ generationConfig: {
+ temperature: 0,
+ },
+ })
+ })
+
+ it("should handle API errors for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockError = new Error("Vertex API error")
const mockCreate = jest.fn().mockRejectedValue(mockError)
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
"Vertex completion error: Vertex API error",
)
})
- it("should handle non-text content", async () => {
+ it("should handle API errors for Gemini", async () => {
+ const mockGemini = require("@google-cloud/vertexai")
+ const mockGenerateContent = mockGemini.VertexAI().getGenerativeModel().generateContent
+ mockGenerateContent.mockRejectedValue(new Error("Vertex API error"))
+ handler = new VertexHandler({
+ apiModelId: "gemini-1.5-pro-001",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
+ await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
+ "Vertex completion error: Vertex API error",
+ )
+ })
+
+ it("should handle non-text content for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockCreate = jest.fn().mockResolvedValue({
content: [{ type: "image" }],
})
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
- it("should handle empty response", async () => {
+ it("should handle empty response for Claude", async () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const mockCreate = jest.fn().mockResolvedValue({
content: [{ type: "text", text: "" }],
})
- ;(handler["client"].messages as any).create = mockCreate
+ ;(handler["anthropicClient"].messages as any).create = mockCreate
+
+ const result = await handler.completePrompt("Test prompt")
+ expect(result).toBe("")
+ })
+
+ it("should handle empty response for Gemini", async () => {
+ const mockGemini = require("@google-cloud/vertexai")
+ const mockGenerateContent = mockGemini.VertexAI().getGenerativeModel().generateContent
+ mockGenerateContent.mockResolvedValue({
+ response: {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: "" }],
+ },
+ },
+ ],
+ },
+ })
+ handler = new VertexHandler({
+ apiModelId: "gemini-1.5-pro-001",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
@@ -615,7 +863,13 @@ describe("VertexHandler", () => {
})
describe("getModel", () => {
- it("should return correct model info", () => {
+ it("should return correct model info for Claude", () => {
+ handler = new VertexHandler({
+ apiModelId: "claude-3-5-sonnet-v2@20241022",
+ vertexProjectId: "test-project",
+ vertexRegion: "us-central1",
+ })
+
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-3-5-sonnet-v2@20241022")
expect(modelInfo.info).toBeDefined()
@@ -623,14 +877,18 @@ describe("VertexHandler", () => {
expect(modelInfo.info.contextWindow).toBe(200_000)
})
- it("should return default model if invalid model specified", () => {
- const invalidHandler = new VertexHandler({
- apiModelId: "invalid-model",
+ it("should return correct model info for Gemini", () => {
+ handler = new VertexHandler({
+ apiModelId: "gemini-2.0-flash-001",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})
- const modelInfo = invalidHandler.getModel()
- expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model
+
+ const modelInfo = handler.getModel()
+ expect(modelInfo.id).toBe("gemini-2.0-flash-001")
+ expect(modelInfo.info).toBeDefined()
+ expect(modelInfo.info.maxTokens).toBe(8192)
+ expect(modelInfo.info.contextWindow).toBe(1048576)
})
})
@@ -724,7 +982,7 @@ describe("VertexHandler", () => {
},
}
})
- ;(thinkingHandler["client"].messages as any).create = mockCreate
+ ;(thinkingHandler["anthropicClient"].messages as any).create = mockCreate
await thinkingHandler
.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }])
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index a25fad07ee..5b8fc1ad78 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -5,6 +5,8 @@ import { ApiHandler, SingleCompletionHandler } from "../"
import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
+import { VertexAI } from "@google-cloud/vertexai"
+import { convertAnthropicMessageToVertexGemini } from "../transform/vertex-gemini-format"
// Types for Vertex SDK
@@ -91,19 +93,37 @@ interface VertexMessageStreamEvent {
thinking: string
}
}
-
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler, SingleCompletionHandler {
+ MODEL_CLAUDE = "claude"
+ MODEL_GEMINI = "gemini"
+
private options: ApiHandlerOptions
- private client: AnthropicVertex
+ private anthropicClient: AnthropicVertex
+ private geminiClient: VertexAI
+ private modelType: string
constructor(options: ApiHandlerOptions) {
this.options = options
- this.client = new AnthropicVertex({
+
+ if (this.options.apiModelId?.startsWith(this.MODEL_CLAUDE)) {
+ this.modelType = this.MODEL_CLAUDE
+ } else if (this.options.apiModelId?.startsWith(this.MODEL_GEMINI)) {
+ this.modelType = this.MODEL_GEMINI
+ } else {
+ throw new Error(`Unknown model ID: ${this.options.apiModelId}`)
+ }
+
+ this.anthropicClient = new AnthropicVertex({
projectId: this.options.vertexProjectId ?? "not-provided",
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion ?? "us-east5",
})
+
+ this.geminiClient = new VertexAI({
+ project: this.options.vertexProjectId ?? "not-provided",
+ location: this.options.vertexRegion ?? "us-east5",
+ })
}
private formatMessageForCache(message: Anthropic.Messages.MessageParam, shouldCache: boolean): VertexMessage {
@@ -154,7 +174,42 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
}
- async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ private async *createGeminiMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ const model = this.geminiClient.getGenerativeModel({
+ model: this.getModel().id,
+ systemInstruction: systemPrompt,
+ })
+
+ const result = await model.generateContentStream({
+ contents: messages.map(convertAnthropicMessageToVertexGemini),
+ generationConfig: {
+ maxOutputTokens: this.getModel().info.maxTokens,
+ temperature: this.options.modelTemperature ?? 0,
+ },
+ })
+
+ for await (const chunk of result.stream) {
+ if (chunk.candidates?.[0]?.content?.parts) {
+ for (const part of chunk.candidates[0].content.parts) {
+ if (part.text) {
+ yield {
+ type: "text",
+ text: part.text,
+ }
+ }
+ }
+ }
+ }
+
+ const response = await result.response
+ yield {
+ type: "usage",
+ inputTokens: response.usageMetadata?.promptTokenCount ?? 0,
+ outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0,
+ }
+ }
+
+ private async *createClaudeMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let { id, info, temperature, maxTokens, thinking } = model
const useCache = model.info.supportsPromptCache
@@ -192,7 +247,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
stream: true,
}
- const stream = (await this.client.messages.create(
+ const stream = (await this.anthropicClient.messages.create(
params as Anthropic.Messages.MessageCreateParamsStreaming,
)) as unknown as AnthropicStream
@@ -272,6 +327,22 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
}
+ async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ switch (this.modelType) {
+ case this.MODEL_CLAUDE: {
+ yield* this.createClaudeMessage(systemPrompt, messages)
+ break
+ }
+ case this.MODEL_GEMINI: {
+ yield* this.createGeminiMessage(systemPrompt, messages)
+ break
+ }
+ default: {
+ throw new Error(`Invalid model type: ${this.modelType}`)
+ }
+ }
+ }
+
getModel(): {
id: VertexModelId
info: ModelInfo
@@ -316,7 +387,36 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
return { id, info, temperature, maxTokens, thinking }
}
- async completePrompt(prompt: string): Promise {
+ private async completePromptGemini(prompt: string): Promise {
+ try {
+ const model = this.geminiClient.getGenerativeModel({
+ model: this.getModel().id,
+ })
+
+ const result = await model.generateContent({
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ generationConfig: {
+ temperature: this.options.modelTemperature ?? 0,
+ },
+ })
+
+ let text = ""
+ result.response.candidates?.forEach((candidate) => {
+ candidate.content.parts.forEach((part) => {
+ text += part.text
+ })
+ })
+
+ return text
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Vertex completion error: ${error.message}`)
+ }
+ throw error
+ }
+ }
+
+ private async completePromptClaude(prompt: string): Promise {
try {
let { id, info, temperature, maxTokens, thinking } = this.getModel()
const useCache = info.supportsPromptCache
@@ -344,7 +444,7 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
stream: false,
}
- const response = (await this.client.messages.create(
+ const response = (await this.anthropicClient.messages.create(
params as Anthropic.Messages.MessageCreateParamsNonStreaming,
)) as unknown as VertexMessageResponse
@@ -360,4 +460,18 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
throw error
}
}
+
+ async completePrompt(prompt: string): Promise {
+ switch (this.modelType) {
+ case this.MODEL_CLAUDE: {
+ return this.completePromptClaude(prompt)
+ }
+ case this.MODEL_GEMINI: {
+ return this.completePromptGemini(prompt)
+ }
+ default: {
+ throw new Error(`Invalid model type: ${this.modelType}`)
+ }
+ }
+ }
}
diff --git a/src/api/transform/__tests__/vertex-gemini-format.test.ts b/src/api/transform/__tests__/vertex-gemini-format.test.ts
new file mode 100644
index 0000000000..bcb26df099
--- /dev/null
+++ b/src/api/transform/__tests__/vertex-gemini-format.test.ts
@@ -0,0 +1,338 @@
+// npx jest src/api/transform/__tests__/vertex-gemini-format.test.ts
+
+import { Anthropic } from "@anthropic-ai/sdk"
+
+import { convertAnthropicMessageToVertexGemini } from "../vertex-gemini-format"
+
+describe("convertAnthropicMessageToVertexGemini", () => {
+ it("should convert a simple text message", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: "Hello, world!",
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [{ text: "Hello, world!" }],
+ })
+ })
+
+ it("should convert assistant role to model role", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "assistant",
+ content: "I'm an assistant",
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "model",
+ parts: [{ text: "I'm an assistant" }],
+ })
+ })
+
+ it("should convert a message with text blocks", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ { type: "text", text: "First paragraph" },
+ { type: "text", text: "Second paragraph" },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [{ text: "First paragraph" }, { text: "Second paragraph" }],
+ })
+ })
+
+ it("should convert a message with an image", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ { type: "text", text: "Check out this image:" },
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/jpeg",
+ data: "base64encodeddata",
+ },
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [
+ { text: "Check out this image:" },
+ {
+ inlineData: {
+ data: "base64encodeddata",
+ mimeType: "image/jpeg",
+ },
+ },
+ ],
+ })
+ })
+
+ it("should throw an error for unsupported image source type", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "image",
+ source: {
+ type: "url", // Not supported
+ url: "https://example.com/image.jpg",
+ } as any,
+ },
+ ],
+ }
+
+ expect(() => convertAnthropicMessageToVertexGemini(anthropicMessage)).toThrow("Unsupported image source type")
+ })
+
+ it("should convert a message with tool use", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "assistant",
+ content: [
+ { type: "text", text: "Let me calculate that for you." },
+ {
+ type: "tool_use",
+ id: "calc-123",
+ name: "calculator",
+ input: { operation: "add", numbers: [2, 3] },
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "model",
+ parts: [
+ { text: "Let me calculate that for you." },
+ {
+ functionCall: {
+ name: "calculator",
+ args: { operation: "add", numbers: [2, 3] },
+ },
+ },
+ ],
+ })
+ })
+
+ it("should convert a message with tool result as string", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ { type: "text", text: "Here's the result:" },
+ {
+ type: "tool_result",
+ tool_use_id: "calculator-123",
+ content: "The result is 5",
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [
+ { text: "Here's the result:" },
+ {
+ functionResponse: {
+ name: "calculator",
+ response: {
+ name: "calculator",
+ content: "The result is 5",
+ },
+ },
+ },
+ ],
+ })
+ })
+
+ it("should handle empty tool result content", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "calculator-123",
+ content: null as any, // Empty content
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ // Should skip the empty tool result
+ expect(result).toEqual({
+ role: "user",
+ parts: [],
+ })
+ })
+
+ it("should convert a message with tool result as array with text only", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "search-123",
+ content: [
+ { type: "text", text: "First result" },
+ { type: "text", text: "Second result" },
+ ],
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [
+ {
+ functionResponse: {
+ name: "search",
+ response: {
+ name: "search",
+ content: "First result\n\nSecond result",
+ },
+ },
+ },
+ ],
+ })
+ })
+
+ it("should convert a message with tool result as array with text and images", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "search-123",
+ content: [
+ { type: "text", text: "Search results:" },
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/png",
+ data: "image1data",
+ },
+ },
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/jpeg",
+ data: "image2data",
+ },
+ },
+ ],
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [
+ {
+ functionResponse: {
+ name: "search",
+ response: {
+ name: "search",
+ content: "Search results:\n\n(See next part for image)",
+ },
+ },
+ },
+ {
+ inlineData: {
+ data: "image1data",
+ mimeType: "image/png",
+ },
+ },
+ {
+ inlineData: {
+ data: "image2data",
+ mimeType: "image/jpeg",
+ },
+ },
+ ],
+ })
+ })
+
+ it("should convert a message with tool result containing only images", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "imagesearch-123",
+ content: [
+ {
+ type: "image",
+ source: {
+ type: "base64",
+ media_type: "image/png",
+ data: "onlyimagedata",
+ },
+ },
+ ],
+ },
+ ],
+ }
+
+ const result = convertAnthropicMessageToVertexGemini(anthropicMessage)
+
+ expect(result).toEqual({
+ role: "user",
+ parts: [
+ {
+ functionResponse: {
+ name: "imagesearch",
+ response: {
+ name: "imagesearch",
+ content: "\n\n(See next part for image)",
+ },
+ },
+ },
+ {
+ inlineData: {
+ data: "onlyimagedata",
+ mimeType: "image/png",
+ },
+ },
+ ],
+ })
+ })
+
+ it("should throw an error for unsupported content block type", () => {
+ const anthropicMessage: Anthropic.Messages.MessageParam = {
+ role: "user",
+ content: [
+ {
+ type: "unknown_type", // Unsupported type
+ data: "some data",
+ } as any,
+ ],
+ }
+
+ expect(() => convertAnthropicMessageToVertexGemini(anthropicMessage)).toThrow(
+ "Unsupported content block type: unknown_type",
+ )
+ })
+})
diff --git a/src/api/transform/vertex-gemini-format.ts b/src/api/transform/vertex-gemini-format.ts
new file mode 100644
index 0000000000..75abb7d3be
--- /dev/null
+++ b/src/api/transform/vertex-gemini-format.ts
@@ -0,0 +1,83 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+import { Content, FunctionCallPart, FunctionResponsePart, InlineDataPart, Part, TextPart } from "@google-cloud/vertexai"
+
+function convertAnthropicContentToVertexGemini(content: Anthropic.Messages.MessageParam["content"]): Part[] {
+ if (typeof content === "string") {
+ return [{ text: content } as TextPart]
+ }
+
+ return content.flatMap((block) => {
+ switch (block.type) {
+ case "text":
+ return { text: block.text } as TextPart
+ case "image":
+ if (block.source.type !== "base64") {
+ throw new Error("Unsupported image source type")
+ }
+ return {
+ inlineData: {
+ data: block.source.data,
+ mimeType: block.source.media_type,
+ },
+ } as InlineDataPart
+ case "tool_use":
+ return {
+ functionCall: {
+ name: block.name,
+ args: block.input,
+ },
+ } as FunctionCallPart
+ case "tool_result":
+ const name = block.tool_use_id.split("-")[0]
+ if (!block.content) {
+ return []
+ }
+ if (typeof block.content === "string") {
+ return {
+ functionResponse: {
+ name,
+ response: {
+ name,
+ content: block.content,
+ },
+ },
+ } as FunctionResponsePart
+ } else {
+ // The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
+ const textParts = block.content.filter((part) => part.type === "text")
+ const imageParts = block.content.filter((part) => part.type === "image")
+ const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
+ const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
+ return [
+ {
+ functionResponse: {
+ name,
+ response: {
+ name,
+ content: text + imageText,
+ },
+ },
+ } as FunctionResponsePart,
+ ...imageParts.map(
+ (part) =>
+ ({
+ inlineData: {
+ data: part.source.data,
+ mimeType: part.source.media_type,
+ },
+ }) as InlineDataPart,
+ ),
+ ]
+ }
+ default:
+ throw new Error(`Unsupported content block type: ${(block as any).type}`)
+ }
+ })
+}
+
+export function convertAnthropicMessageToVertexGemini(message: Anthropic.Messages.MessageParam): Content {
+ return {
+ role: message.role === "assistant" ? "model" : "user",
+ parts: convertAnthropicContentToVertexGemini(message.content),
+ }
+}
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 99e2986e88..b16e5142a0 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -436,6 +436,46 @@ export const openRouterDefaultModelInfo: ModelInfo = {
export type VertexModelId = keyof typeof vertexModels
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
export const vertexModels = {
+ "gemini-2.0-flash-001": {
+ maxTokens: 8192,
+ contextWindow: 1_048_576,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 0.15,
+ outputPrice: 0.6,
+ },
+ "gemini-2.0-flash-lite-001": {
+ maxTokens: 8192,
+ contextWindow: 1_048_576,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 0.075,
+ outputPrice: 0.3,
+ },
+ "gemini-2.0-flash-thinking-exp-01-21": {
+ maxTokens: 8192,
+ contextWindow: 32_768,
+ 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.075,
+ outputPrice: 0.3,
+ },
+ "gemini-1.5-pro-002": {
+ maxTokens: 8192,
+ contextWindow: 2_097_152,
+ supportsImages: true,
+ supportsPromptCache: false,
+ inputPrice: 1.25,
+ outputPrice: 5,
+ },
"claude-3-7-sonnet@20250219:thinking": {
maxTokens: 64_000,
contextWindow: 200_000,
From 1d3f5380a80d09ac747d93c701473215bdc03ba6 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 21:23:11 -0800
Subject: [PATCH 097/298] Move integration tests into its own module
---
.github/workflows/changeset-release.yml | 2 +-
.github/workflows/code-qa.yml | 18 +-
.vscodeignore | 3 +-
.../.env.integration.example | 0
.vscode-test.mjs => e2e/.vscode-test.mjs | 2 +-
{src/test => e2e}/VSCODE_INTEGRATION_TESTS.md | 4 +-
e2e/package-lock.json | 2387 +++++++++++++++++
e2e/package.json | 21 +
{src/test => e2e/src}/runTest.ts | 2 +-
{src/test => e2e/src}/suite/extension.test.ts | 0
{src/test => e2e/src}/suite/index.ts | 3 +-
{src/test => e2e/src}/suite/modes.test.ts | 0
{src/test => e2e/src}/suite/task.test.ts | 0
.../tsconfig.json | 7 +-
knip.json | 4 +-
package-lock.json | 922 +------
package.json | 33 +-
src/exports/cline.d.ts | 93 +
src/shared/modes.ts | 6 +-
19 files changed, 2548 insertions(+), 959 deletions(-)
rename .env.integration.example => e2e/.env.integration.example (100%)
rename .vscode-test.mjs => e2e/.vscode-test.mjs (89%)
rename {src/test => e2e}/VSCODE_INTEGRATION_TESTS.md (98%)
create mode 100644 e2e/package-lock.json
create mode 100644 e2e/package.json
rename {src/test => e2e/src}/runTest.ts (98%)
rename {src/test => e2e/src}/suite/extension.test.ts (100%)
rename {src/test => e2e/src}/suite/index.ts (95%)
rename {src/test => e2e/src}/suite/modes.test.ts (100%)
rename {src/test => e2e/src}/suite/task.test.ts (100%)
rename tsconfig.integration.json => e2e/tsconfig.json (60%)
diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml
index a2bcd3f039..462516365b 100644
--- a/.github/workflows/changeset-release.yml
+++ b/.github/workflows/changeset-release.yml
@@ -37,7 +37,7 @@ jobs:
cache: 'npm'
- name: Install Dependencies
- run: npm run install:all
+ run: npm run install:ci
# Check if there are any new changesets to process
- name: Check for changesets
diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml
index fde891f804..b7292dd9ee 100644
--- a/.github/workflows/code-qa.yml
+++ b/.github/workflows/code-qa.yml
@@ -20,7 +20,7 @@ jobs:
node-version: '18'
cache: 'npm'
- name: Install dependencies
- run: npm run install:all
+ run: npm run install:ci
- name: Compile
run: npm run compile
- name: Check types
@@ -39,7 +39,7 @@ jobs:
node-version: '18'
cache: 'npm'
- name: Install dependencies
- run: npm run install:all
+ run: npm run install:ci
- name: Run knip checks
run: npm run knip
@@ -54,7 +54,7 @@ jobs:
node-version: '18'
cache: 'npm'
- name: Install dependencies
- run: npm run install:all
+ run: npm run install:ci
- name: Run unit tests
run: npx jest --silent
@@ -69,7 +69,7 @@ jobs:
node-version: '18'
cache: 'npm'
- name: Install dependencies
- run: npm run install:all
+ run: npm run install:ci
- name: Run unit tests
working-directory: webview-ui
run: npx jest --silent
@@ -108,9 +108,11 @@ jobs:
with:
node-version: '18'
cache: 'npm'
- - name: Create env.integration file
- run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
- name: Install dependencies
- run: npm run install:all
+ run: npm run install:ci
+ - name: Create env.integration file
+ working-directory: e2e
+ run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
- name: Run integration tests
- run: xvfb-run -a npm run test:integration
+ working-directory: e2e
+ run: xvfb-run -a npm run ci
diff --git a/.vscodeignore b/.vscodeignore
index 638ac22db7..1fc5a728b0 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -4,6 +4,8 @@
.vscode/**
.vscode-test/**
out/**
+out-integration/**
+e2e/**
node_modules/**
src/**
.gitignore
@@ -25,7 +27,6 @@ demo.gif
.roomodes
cline_docs/**
coverage/**
-out-integration/**
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
diff --git a/.env.integration.example b/e2e/.env.integration.example
similarity index 100%
rename from .env.integration.example
rename to e2e/.env.integration.example
diff --git a/.vscode-test.mjs b/e2e/.vscode-test.mjs
similarity index 89%
rename from .vscode-test.mjs
rename to e2e/.vscode-test.mjs
index dd7760789b..16ea4271ba 100644
--- a/.vscode-test.mjs
+++ b/e2e/.vscode-test.mjs
@@ -6,7 +6,7 @@ import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
label: 'integrationTest',
- files: 'out-integration/test/**/*.test.js',
+ files: 'out/e2e/src/suite/**/*.test.js',
workspaceFolder: '.',
mocha: {
ui: 'tdd',
diff --git a/src/test/VSCODE_INTEGRATION_TESTS.md b/e2e/VSCODE_INTEGRATION_TESTS.md
similarity index 98%
rename from src/test/VSCODE_INTEGRATION_TESTS.md
rename to e2e/VSCODE_INTEGRATION_TESTS.md
index f5882fea1e..25f54492de 100644
--- a/src/test/VSCODE_INTEGRATION_TESTS.md
+++ b/e2e/VSCODE_INTEGRATION_TESTS.md
@@ -11,8 +11,8 @@ The integration tests use the `@vscode/test-electron` package to run tests in a
### Directory Structure
```
-src/test/
-├── runTest.ts # Main test runner
+e2e/src/
+├── runTest.ts # Main test runner
├── suite/
│ ├── index.ts # Test suite configuration
│ ├── modes.test.ts # Mode switching tests
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
new file mode 100644
index 0000000000..278df120c2
--- /dev/null
+++ b/e2e/package-lock.json
@@ -0,0 +1,2387 @@
+{
+ "name": "e2e",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "e2e",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@vscode/test-cli": "^0.0.9",
+ "@vscode/test-electron": "^2.4.0",
+ "mocha": "^11.1.0",
+ "typescript": "^5.4.5"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
+ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vscode/test-cli": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.9.tgz",
+ "integrity": "sha512-vsl5/ueE3Jf0f6XzB0ECHHMsd5A0Yu6StElb8a+XsubZW7kHNAOw4Y3TSSuDzKEpLnJ92nbMy1Zl+KLGCE6NaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mocha": "^10.0.2",
+ "c8": "^9.1.0",
+ "chokidar": "^3.5.3",
+ "enhanced-resolve": "^5.15.0",
+ "glob": "^10.3.10",
+ "minimatch": "^9.0.3",
+ "mocha": "^10.2.0",
+ "supports-color": "^9.4.0",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "vscode-test": "out/bin.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vscode/test-cli/node_modules/mocha": {
+ "version": "10.8.2",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
+ "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.3",
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^3.5.3",
+ "debug": "^4.3.5",
+ "diff": "^5.2.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^8.1.0",
+ "he": "^1.2.0",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^5.1.6",
+ "ms": "^2.1.3",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^6.5.1",
+ "yargs": "^16.2.0",
+ "yargs-parser": "^20.2.9",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@vscode/test-cli/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@vscode/test-electron": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz",
+ "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "jszip": "^3.10.1",
+ "ora": "^7.0.1",
+ "semver": "^7.6.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/c8": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
+ "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@istanbuljs/schema": "^0.1.3",
+ "find-up": "^5.0.0",
+ "foreground-child": "^3.1.1",
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.1.6",
+ "test-exclude": "^6.0.0",
+ "v8-to-istanbul": "^9.0.0",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "c8": "bin/c8.js"
+ },
+ "engines": {
+ "node": ">=14.14.0"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
+ "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
+ "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+ "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dev": true,
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz",
+ "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.3",
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^3.5.3",
+ "debug": "^4.3.5",
+ "diff": "^5.2.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^10.4.5",
+ "he": "^1.2.0",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^5.1.6",
+ "ms": "^2.1.3",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^6.5.1",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz",
+ "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "cli-cursor": "^4.0.0",
+ "cli-spinners": "^2.9.0",
+ "is-interactive": "^2.0.0",
+ "is-unicode-supported": "^1.3.0",
+ "log-symbols": "^5.1.0",
+ "stdin-discarder": "^0.1.0",
+ "string-width": "^6.1.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ora/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ora/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/log-symbols": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
+ "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.0.0",
+ "is-unicode-supported": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/string-width": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz",
+ "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^10.2.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true,
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
+ "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/stdin-discarder": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
+ "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "9.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
+ "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+ "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/e2e/package.json b/e2e/package.json
new file mode 100644
index 0000000000..7e0daac064
--- /dev/null
+++ b/e2e/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "e2e",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "build": "cd .. && npm run build",
+ "compile": "tsc -p tsconfig.json",
+ "lint": "eslint src --ext ts",
+ "check-types": "tsc --noEmit",
+ "test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/e2e/src/runTest.js",
+ "ci": "npm run build && npm run test"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@vscode/test-cli": "^0.0.9",
+ "@vscode/test-electron": "^2.4.0",
+ "mocha": "^11.1.0",
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/src/test/runTest.ts b/e2e/src/runTest.ts
similarity index 98%
rename from src/test/runTest.ts
rename to e2e/src/runTest.ts
index 2576c6072f..28545efd46 100644
--- a/src/test/runTest.ts
+++ b/e2e/src/runTest.ts
@@ -6,7 +6,7 @@ async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
- const extensionDevelopmentPath = path.resolve(__dirname, "../../")
+ const extensionDevelopmentPath = path.resolve(__dirname, "../../../../")
// The path to the extension test script
// Passed to --extensionTestsPath
diff --git a/src/test/suite/extension.test.ts b/e2e/src/suite/extension.test.ts
similarity index 100%
rename from src/test/suite/extension.test.ts
rename to e2e/src/suite/extension.test.ts
diff --git a/src/test/suite/index.ts b/e2e/src/suite/index.ts
similarity index 95%
rename from src/test/suite/index.ts
rename to e2e/src/suite/index.ts
index cc487b0bf7..a9540d9600 100644
--- a/src/test/suite/index.ts
+++ b/e2e/src/suite/index.ts
@@ -1,8 +1,7 @@
import * as path from "path"
import Mocha from "mocha"
import { glob } from "glob"
-import { ClineAPI } from "../../exports/cline"
-import { ClineProvider } from "../../core/webview/ClineProvider"
+import { ClineAPI, ClineProvider } from "../../../src/exports/cline"
import * as vscode from "vscode"
declare global {
diff --git a/src/test/suite/modes.test.ts b/e2e/src/suite/modes.test.ts
similarity index 100%
rename from src/test/suite/modes.test.ts
rename to e2e/src/suite/modes.test.ts
diff --git a/src/test/suite/task.test.ts b/e2e/src/suite/task.test.ts
similarity index 100%
rename from src/test/suite/task.test.ts
rename to e2e/src/suite/task.test.ts
diff --git a/tsconfig.integration.json b/e2e/tsconfig.json
similarity index 60%
rename from tsconfig.integration.json
rename to e2e/tsconfig.json
index 0de0ea736a..9958110767 100644
--- a/tsconfig.integration.json
+++ b/e2e/tsconfig.json
@@ -9,9 +9,8 @@
"strict": true,
"skipLibCheck": true,
"useUnknownInCatchVariables": false,
- "rootDir": "src",
- "outDir": "out-integration"
+ "outDir": "out"
},
- "include": ["**/*.ts"],
- "exclude": [".vscode-test", "benchmark", "dist", "**/node_modules/**", "out", "out-integration", "webview-ui"]
+ "include": ["src", "../src/exports"],
+ "exclude": [".vscode-test", "**/node_modules/**", "out"]
}
diff --git a/knip.json b/knip.json
index b0e0839da7..a9f0b93e0d 100644
--- a/knip.json
+++ b/knip.json
@@ -16,7 +16,9 @@
"src/activate/**",
"src/exports/**",
"src/extension.ts",
- ".vscode-test.mjs"
+ "e2e/.vscode-test.mjs",
+ "e2e/src/runTest.ts",
+ "e2e/src/suite/index.ts"
],
"workspaces": {
"webview-ui": {
diff --git a/package-lock.json b/package-lock.json
index 950769b39b..a8fa18b5f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -63,13 +63,10 @@
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
- "@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
- "@vscode/test-cli": "^0.0.9",
- "@vscode/test-electron": "^2.4.0",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"glob": "^11.0.1",
@@ -79,7 +76,6 @@
"knip": "^5.44.4",
"lint-staged": "^15.2.11",
"mkdirp": "^3.0.1",
- "mocha": "^11.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
@@ -4153,17 +4149,6 @@
"node": ">= 8"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@puppeteer/browsers": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.5.0.tgz",
@@ -6032,13 +6017,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/mocha": {
- "version": "10.0.10",
- "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
- "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -6351,332 +6329,6 @@
"resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz",
"integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ=="
},
- "node_modules/@vscode/test-cli": {
- "version": "0.0.9",
- "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.9.tgz",
- "integrity": "sha512-vsl5/ueE3Jf0f6XzB0ECHHMsd5A0Yu6StElb8a+XsubZW7kHNAOw4Y3TSSuDzKEpLnJ92nbMy1Zl+KLGCE6NaA==",
- "dev": true,
- "dependencies": {
- "@types/mocha": "^10.0.2",
- "c8": "^9.1.0",
- "chokidar": "^3.5.3",
- "enhanced-resolve": "^5.15.0",
- "glob": "^10.3.10",
- "minimatch": "^9.0.3",
- "mocha": "^10.2.0",
- "supports-color": "^9.4.0",
- "yargs": "^17.7.2"
- },
- "bin": {
- "vscode-test": "out/bin.mjs"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/cliui": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
- "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^7.0.0"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vscode/test-cli/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/@vscode/test-cli/node_modules/mocha": {
- "version": "10.8.2",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
- "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-colors": "^4.1.3",
- "browser-stdout": "^1.3.1",
- "chokidar": "^3.5.3",
- "debug": "^4.3.5",
- "diff": "^5.2.0",
- "escape-string-regexp": "^4.0.0",
- "find-up": "^5.0.0",
- "glob": "^8.1.0",
- "he": "^1.2.0",
- "js-yaml": "^4.1.0",
- "log-symbols": "^4.1.0",
- "minimatch": "^5.1.6",
- "ms": "^2.1.3",
- "serialize-javascript": "^6.0.2",
- "strip-json-comments": "^3.1.1",
- "supports-color": "^8.1.1",
- "workerpool": "^6.5.1",
- "yargs": "^16.2.0",
- "yargs-parser": "^20.2.9",
- "yargs-unparser": "^2.0.0"
- },
- "bin": {
- "_mocha": "bin/_mocha",
- "mocha": "bin/mocha.js"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/glob": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
- "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^5.0.1",
- "once": "^1.3.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/mocha/node_modules/yargs": {
- "version": "16.2.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
- "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^7.0.2",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.0",
- "y18n": "^5.0.5",
- "yargs-parser": "^20.2.2"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/@vscode/test-cli/node_modules/yargs-parser": {
- "version": "20.2.9",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
- "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@vscode/test-electron": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz",
- "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==",
- "dev": true,
- "dependencies": {
- "http-proxy-agent": "^7.0.2",
- "https-proxy-agent": "^7.0.5",
- "jszip": "^3.10.1",
- "ora": "^7.0.1",
- "semver": "^7.6.2"
- },
- "engines": {
- "node": ">=16"
- }
- },
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -7179,43 +6831,6 @@
"node": "*"
}
},
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/bl": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
- "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
- "dev": true,
- "dependencies": {
- "buffer": "^6.0.3",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "node_modules/bl/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dev": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
@@ -7251,12 +6866,6 @@
"node": ">=8"
}
},
- "node_modules/browser-stdout": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
- "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
- "dev": true
- },
"node_modules/browserslist": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
@@ -7310,30 +6919,6 @@
"node-int64": "^0.4.0"
}
},
- "node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -7361,31 +6946,6 @@
"node": ">= 0.8"
}
},
- "node_modules/c8": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz",
- "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==",
- "dev": true,
- "dependencies": {
- "@bcoe/v8-coverage": "^0.2.3",
- "@istanbuljs/schema": "^0.1.3",
- "find-up": "^5.0.0",
- "foreground-child": "^3.1.1",
- "istanbul-lib-coverage": "^3.2.0",
- "istanbul-lib-report": "^3.0.1",
- "istanbul-reports": "^3.1.6",
- "test-exclude": "^6.0.0",
- "v8-to-istanbul": "^9.0.0",
- "yargs": "^17.7.2",
- "yargs-parser": "^21.1.1"
- },
- "bin": {
- "c8": "bin/c8.js"
- },
- "engines": {
- "node": ">=14.14.0"
- }
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -7595,33 +7155,6 @@
"node": ">=6"
}
},
- "node_modules/cli-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
- "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
- "dev": true,
- "dependencies": {
- "restore-cursor": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-spinners": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
- "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
- "dev": true,
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
@@ -7980,18 +7513,6 @@
}
}
},
- "node_modules/decamelize": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
- "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/dedent": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@@ -9197,15 +8718,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/flat": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
- "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
- "dev": true,
- "bin": {
- "flat": "cli.js"
- }
- },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -9863,15 +9375,6 @@
"node": ">= 0.4"
}
},
- "node_modules/he": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true,
- "bin": {
- "he": "bin/he"
- }
- },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -10169,18 +9672,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-boolean-object": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.0.tgz",
@@ -10320,18 +9811,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-interactive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
- "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -10389,15 +9868,6 @@
"node": ">=8"
}
},
- "node_modules/is-plain-obj": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
- "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -10525,18 +9995,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-unicode-supported": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
- "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -12077,22 +11535,6 @@
"integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
"dev": true
},
- "node_modules/log-symbols": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
- "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
- "dev": true,
- "dependencies": {
- "chalk": "^4.1.0",
- "is-unicode-supported": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
@@ -12480,183 +11922,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/mocha": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz",
- "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-colors": "^4.1.3",
- "browser-stdout": "^1.3.1",
- "chokidar": "^3.5.3",
- "debug": "^4.3.5",
- "diff": "^5.2.0",
- "escape-string-regexp": "^4.0.0",
- "find-up": "^5.0.0",
- "glob": "^10.4.5",
- "he": "^1.2.0",
- "js-yaml": "^4.1.0",
- "log-symbols": "^4.1.0",
- "minimatch": "^5.1.6",
- "ms": "^2.1.3",
- "serialize-javascript": "^6.0.2",
- "strip-json-comments": "^3.1.1",
- "supports-color": "^8.1.1",
- "workerpool": "^6.5.1",
- "yargs": "^17.7.2",
- "yargs-parser": "^21.1.1",
- "yargs-unparser": "^2.0.0"
- },
- "bin": {
- "_mocha": "bin/_mocha",
- "mocha": "bin/mocha.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/mocha/node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/mocha/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/mocha/node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/mocha/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/mocha/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/mocha/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/mocha/node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/mocha/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
"node_modules/monaco-vscode-textmate-theme-converter": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/monaco-vscode-textmate-theme-converter/-/monaco-vscode-textmate-theme-converter-0.1.7.tgz",
@@ -12801,6 +12066,7 @@
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
"integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"chalk": "^2.4.1",
@@ -13136,92 +12402,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/ora": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz",
- "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==",
- "dev": true,
- "dependencies": {
- "chalk": "^5.3.0",
- "cli-cursor": "^4.0.0",
- "cli-spinners": "^2.9.0",
- "is-interactive": "^2.0.0",
- "is-unicode-supported": "^1.3.0",
- "log-symbols": "^5.1.0",
- "stdin-discarder": "^0.1.0",
- "string-width": "^6.1.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora/node_modules/chalk": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
- "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
- "dev": true,
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/ora/node_modules/emoji-regex": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
- "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
- "dev": true
- },
- "node_modules/ora/node_modules/is-unicode-supported": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
- "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora/node_modules/log-symbols": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
- "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==",
- "dev": true,
- "dependencies": {
- "chalk": "^5.0.0",
- "is-unicode-supported": "^1.1.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ora/node_modules/string-width": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz",
- "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==",
- "dev": true,
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^10.2.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/os-name": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/os-name/-/os-name-6.0.0.tgz",
@@ -13895,15 +13075,6 @@
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
@@ -14152,28 +13323,6 @@
"node": ">=10"
}
},
- "node_modules/restore-cursor": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
- "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
- "dev": true,
- "dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/restore-cursor/node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "dev": true
- },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -14409,15 +13558,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "dev": true,
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -14750,21 +13890,6 @@
"node": ">= 0.8"
}
},
- "node_modules/stdin-discarder": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz",
- "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==",
- "dev": true,
- "dependencies": {
- "bl": "^5.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/streamx": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz",
@@ -15051,18 +14176,6 @@
"integrity": "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==",
"dev": true
},
- "node_modules/supports-color": {
- "version": "9.4.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
- "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -15966,12 +15079,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/workerpool": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
- "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==",
- "dev": true
- },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -16170,33 +15277,6 @@
"node": ">=12"
}
},
- "node_modules/yargs-unparser": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
- "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
- "dev": true,
- "dependencies": {
- "camelcase": "^6.0.0",
- "decamelize": "^4.0.0",
- "flat": "^5.0.2",
- "is-plain-obj": "^2.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yargs-unparser/node_modules/camelcase": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
diff --git a/package.json b/package.json
index fe64af16d6..b9b62bd5e3 100644
--- a/package.json
+++ b/package.json
@@ -276,21 +276,24 @@
"scripts": {
"build": "npm run build:webview && npm run vsix",
"build:webview": "cd webview-ui && npm run build",
- "changeset": "changeset",
- "check-types": "tsc --noEmit && cd webview-ui && npm run check-types",
"compile": "tsc -p . --outDir out && node esbuild.js",
- "compile:integration": "tsc -p tsconfig.integration.json",
- "install:all": "npm install && cd webview-ui && npm install",
- "knip": "knip --include files",
- "lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
- "lint-local": "eslint -c .eslintrc.local.json src --ext ts && npm run lint --prefix webview-ui",
- "lint-fix": "eslint src --ext ts --fix && npm run lint-fix --prefix webview-ui",
- "lint-fix-local": "eslint -c .eslintrc.local.json src --ext ts --fix && npm run lint-fix --prefix webview-ui",
+ "install:all": "npm-run-all -p install-*",
+ "install:ci": "npm install npm-run-all && npm run install:all",
+ "install-extension": "npm install",
+ "install-webview-ui": "cd webview-ui && npm install",
+ "install-e2e": "cd e2e && npm install",
+ "lint": "npm-run-all -p lint:*",
+ "lint:extension": "eslint src --ext ts",
+ "lint:webview-ui": "cd webview-ui && npm run lint",
+ "lint:e2e": "cd e2e && npm run lint",
+ "check-types": "npm-run-all -p check-types:*",
+ "check-types:extension": "tsc --noEmit",
+ "check-types:webview-ui": "cd webview-ui && npm run check-types",
+ "check-types:e2e": "cd e2e && npm run check-types",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
- "pretest": "npm run compile && npm run compile:integration",
+ "pretest": "npm run compile",
"dev": "cd webview-ui && npm run dev",
"test": "jest && cd webview-ui && npm run test",
- "test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- node ./out-integration/test/runTest.js",
"prepare": "husky",
"publish:marketplace": "vsce publish && ovsx publish",
"publish": "npm run build && changeset publish && npm install --package-lock-only",
@@ -300,7 +303,9 @@
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
- "watch-tests": "tsc -p . -w --outDir out"
+ "watch-tests": "tsc -p . -w --outDir out",
+ "changeset": "changeset",
+ "knip": "knip --include files"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
@@ -358,13 +363,10 @@
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
- "@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
- "@vscode/test-cli": "^0.0.9",
- "@vscode/test-electron": "^2.4.0",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"glob": "^11.0.1",
@@ -374,7 +376,6 @@
"knip": "^5.44.4",
"lint-staged": "^15.2.11",
"mkdirp": "^3.0.1",
- "mocha": "^11.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
diff --git a/src/exports/cline.d.ts b/src/exports/cline.d.ts
index fcf93fc10d..e529947b6b 100644
--- a/src/exports/cline.d.ts
+++ b/src/exports/cline.d.ts
@@ -40,3 +40,96 @@ export interface ClineAPI {
*/
sidebarProvider: ClineSidebarProvider
}
+
+export interface ClineProvider {
+ readonly context: vscode.ExtensionContext
+ readonly viewLaunched: boolean
+ readonly messages: ClineMessage[]
+
+ /**
+ * Resolves the webview view for the provider
+ * @param webviewView The webview view or panel to resolve
+ */
+ resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise
+
+ /**
+ * Initializes Cline with a task
+ */
+ initClineWithTask(task?: string, images?: string[]): Promise
+
+ /**
+ * Initializes Cline with a history item
+ */
+ initClineWithHistoryItem(historyItem: HistoryItem): Promise
+
+ /**
+ * Posts a message to the webview
+ */
+ postMessageToWebview(message: ExtensionMessage): Promise
+
+ /**
+ * Handles mode switching
+ */
+ handleModeSwitch(newMode: Mode): Promise
+
+ /**
+ * Updates custom instructions
+ */
+ updateCustomInstructions(instructions?: string): Promise
+
+ /**
+ * Cancels the current task
+ */
+ cancelTask(): Promise
+
+ /**
+ * Clears the current task
+ */
+ clearTask(): Promise
+
+ /**
+ * Gets the current state
+ */
+ getState(): Promise
+
+ /**
+ * Updates a value in the global state
+ * @param key The key to update
+ * @param value The value to set
+ */
+ updateGlobalState(key: GlobalStateKey, value: any): Promise
+
+ /**
+ * Gets a value from the global state
+ * @param key The key to get
+ */
+ getGlobalState(key: GlobalStateKey): Promise
+
+ /**
+ * Stores a secret value in secure storage
+ * @param key The key to store the secret under
+ * @param value The secret value to store, or undefined to remove the secret
+ */
+ storeSecret(key: SecretKey, value?: string): Promise
+
+ /**
+ * Retrieves a secret value from secure storage
+ * @param key The key of the secret to retrieve
+ */
+ getSecret(key: SecretKey): Promise
+
+ /**
+ * Resets the state
+ */
+ resetState(): Promise
+
+ /**
+ * Logs a message
+ */
+ log(message: string): void
+
+ /**
+ * Disposes of the provider
+ */
+ dispose(): Promise
+}
diff --git a/src/shared/modes.ts b/src/shared/modes.ts
index ceb1e3e11f..2eda966519 100644
--- a/src/shared/modes.ts
+++ b/src/shared/modes.ts
@@ -36,7 +36,11 @@ export type CustomModePrompts = {
// Helper to extract group name regardless of format
export function getGroupName(group: GroupEntry): ToolGroup {
- return Array.isArray(group) ? group[0] : group
+ if (typeof group === "string") {
+ return group
+ }
+
+ return group[0]
}
// Helper to get group options if they exist
From 481d613bc8701d880af5af598f5c0d9d75e00935 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 21:34:53 -0800
Subject: [PATCH 098/298] Estimate the number of tokens in the last message for
sliding window math
---
e2e/tsconfig.json | 2 +-
package-lock.json | 9 +
package.json | 1 +
.../__tests__/sliding-window.test.ts | 224 ++++++++++++++++--
src/core/sliding-window/index.ts | 70 +++++-
5 files changed, 276 insertions(+), 30 deletions(-)
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
index 9958110767..792acb14a0 100644
--- a/e2e/tsconfig.json
+++ b/e2e/tsconfig.json
@@ -11,6 +11,6 @@
"useUnknownInCatchVariables": false,
"outDir": "out"
},
- "include": ["src", "../src/exports"],
+ "include": ["src", "../src/exports/cline.d.ts"],
"exclude": [".vscode-test", "**/node_modules/**", "out"]
}
diff --git a/package-lock.json b/package-lock.json
index a8fa18b5f1..0b24ce6664 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"isbinaryfile": "^5.0.2",
+ "js-tiktoken": "^1.0.19",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"openai": "^4.78.1",
@@ -10909,6 +10910,14 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/js-tiktoken": {
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.19.tgz",
+ "integrity": "sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==",
+ "dependencies": {
+ "base64-js": "^1.5.1"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index b9b62bd5e3..6f5699a7c7 100644
--- a/package.json
+++ b/package.json
@@ -348,6 +348,7 @@
"sound-play": "^1.1.0",
"string-similarity": "^4.0.4",
"strip-ansi": "^7.1.0",
+ "js-tiktoken": "^1.0.19",
"tmp": "^0.2.3",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts
index cb897aa8cb..74b734d738 100644
--- a/src/core/sliding-window/__tests__/sliding-window.test.ts
+++ b/src/core/sliding-window/__tests__/sliding-window.test.ts
@@ -3,7 +3,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "../../../shared/api"
-import { truncateConversation, truncateConversationIfNeeded } from "../index"
+import { estimateTokenCount, truncateConversation, truncateConversationIfNeeded } from "../index"
/**
* Tests for the truncateConversation function
@@ -118,23 +118,26 @@ describe("getMaxTokens", () => {
const modelInfo = createModelInfo(100000, 50000)
// Max tokens = 100000 - 50000 = 50000
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// Below max tokens - no truncation
const result1 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 49999,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result1).toEqual(messages)
+ expect(result1).toEqual(messagesWithSmallContent)
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 50001,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result2).not.toEqual(messages)
+ expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -142,23 +145,26 @@ describe("getMaxTokens", () => {
const modelInfo = createModelInfo(100000, undefined)
// Max tokens = 100000 - (100000 * 0.2) = 80000
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// Below max tokens - no truncation
const result1 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 79999,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result1).toEqual(messages)
+ expect(result1).toEqual(messagesWithSmallContent)
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 80001,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result2).not.toEqual(messages)
+ expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -166,23 +172,26 @@ describe("getMaxTokens", () => {
const modelInfo = createModelInfo(50000, 10000)
// Max tokens = 50000 - 10000 = 40000
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// Below max tokens - no truncation
const result1 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 39999,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result1).toEqual(messages)
+ expect(result1).toEqual(messagesWithSmallContent)
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 40001,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result2).not.toEqual(messages)
+ expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
@@ -190,23 +199,26 @@ describe("getMaxTokens", () => {
const modelInfo = createModelInfo(200000, 30000)
// Max tokens = 200000 - 30000 = 170000
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// Below max tokens - no truncation
const result1 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 169999,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result1).toEqual(messages)
+ expect(result1).toEqual(messagesWithSmallContent)
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: 170001,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result2).not.toEqual(messages)
+ expect(result2).not.toEqual(messagesWithSmallContent)
expect(result2.length).toBe(3) // Truncated with 0.5 fraction
})
})
@@ -234,13 +246,16 @@ describe("truncateConversationIfNeeded", () => {
const maxTokens = 100000 - 30000 // 70000
const totalTokens = 69999 // Below threshold
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
const result = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
- expect(result).toEqual(messages) // No truncation occurs
+ expect(result).toEqual(messagesWithSmallContent) // No truncation occurs
})
it("should truncate if tokens are above max tokens threshold", () => {
@@ -248,12 +263,15 @@ describe("truncateConversationIfNeeded", () => {
const maxTokens = 100000 - 30000 // 70000
const totalTokens = 70001 // Above threshold
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// When truncating, always uses 0.5 fraction
// With 4 messages after the first, 0.5 fraction means remove 2 messages
- const expectedResult = [messages[0], messages[3], messages[4]]
+ const expectedResult = [messagesWithSmallContent[0], messagesWithSmallContent[3], messagesWithSmallContent[4]]
const result = truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens,
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
@@ -266,18 +284,21 @@ describe("truncateConversationIfNeeded", () => {
const modelInfo1 = createModelInfo(100000, true, 30000)
const modelInfo2 = createModelInfo(100000, false, 30000)
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
// Test below threshold
const belowThreshold = 69999
expect(
truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: belowThreshold,
contextWindow: modelInfo1.contextWindow,
maxTokens: modelInfo1.maxTokens,
}),
).toEqual(
truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: belowThreshold,
contextWindow: modelInfo2.contextWindow,
maxTokens: modelInfo2.maxTokens,
@@ -288,18 +309,171 @@ describe("truncateConversationIfNeeded", () => {
const aboveThreshold = 70001
expect(
truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: aboveThreshold,
contextWindow: modelInfo1.contextWindow,
maxTokens: modelInfo1.maxTokens,
}),
).toEqual(
truncateConversationIfNeeded({
- messages,
+ messages: messagesWithSmallContent,
totalTokens: aboveThreshold,
contextWindow: modelInfo2.contextWindow,
maxTokens: modelInfo2.maxTokens,
}),
)
})
+
+ it("should consider incoming content when deciding to truncate", () => {
+ const modelInfo = createModelInfo(100000, true, 30000)
+ const maxTokens = 30000
+ const availableTokens = modelInfo.contextWindow - maxTokens
+
+ // Test case 1: Small content that won't push us over the threshold
+ const smallContent = [{ type: "text" as const, text: "Small content" }]
+ const smallContentTokens = estimateTokenCount(smallContent)
+ const messagesWithSmallContent: Anthropic.Messages.MessageParam[] = [
+ ...messages.slice(0, -1),
+ { role: messages[messages.length - 1].role, content: smallContent },
+ ]
+
+ // Set base tokens so total is below threshold even with small content added
+ const baseTokensForSmall = availableTokens - smallContentTokens - 10
+ const resultWithSmall = truncateConversationIfNeeded({
+ messages: messagesWithSmallContent,
+ totalTokens: baseTokensForSmall,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens,
+ })
+ expect(resultWithSmall).toEqual(messagesWithSmallContent) // No truncation
+
+ // Test case 2: Large content that will push us over the threshold
+ const largeContent = [
+ {
+ type: "text" as const,
+ text: "A very large incoming message that would consume a significant number of tokens and push us over the threshold",
+ },
+ ]
+ const largeContentTokens = estimateTokenCount(largeContent)
+ const messagesWithLargeContent: Anthropic.Messages.MessageParam[] = [
+ ...messages.slice(0, -1),
+ { role: messages[messages.length - 1].role, content: largeContent },
+ ]
+
+ // Set base tokens so we're just below threshold without content, but over with content
+ const baseTokensForLarge = availableTokens - Math.floor(largeContentTokens / 2)
+ const resultWithLarge = truncateConversationIfNeeded({
+ messages: messagesWithLargeContent,
+ totalTokens: baseTokensForLarge,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens,
+ })
+ expect(resultWithLarge).not.toEqual(messagesWithLargeContent) // Should truncate
+
+ // Test case 3: Very large content that will definitely exceed threshold
+ const veryLargeContent = [{ type: "text" as const, text: "X".repeat(1000) }]
+ const veryLargeContentTokens = estimateTokenCount(veryLargeContent)
+ const messagesWithVeryLargeContent: Anthropic.Messages.MessageParam[] = [
+ ...messages.slice(0, -1),
+ { role: messages[messages.length - 1].role, content: veryLargeContent },
+ ]
+
+ // Set base tokens so we're just below threshold without content
+ const baseTokensForVeryLarge = availableTokens - Math.floor(veryLargeContentTokens / 2)
+ const resultWithVeryLarge = truncateConversationIfNeeded({
+ messages: messagesWithVeryLargeContent,
+ totalTokens: baseTokensForVeryLarge,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens,
+ })
+ expect(resultWithVeryLarge).not.toEqual(messagesWithVeryLargeContent) // Should truncate
+ })
+})
+/**
+ * Tests for the estimateTokenCount function
+ */
+describe("estimateTokenCount", () => {
+ it("should return 0 for empty or undefined content", () => {
+ expect(estimateTokenCount([])).toBe(0)
+ // @ts-ignore - Testing with undefined
+ expect(estimateTokenCount(undefined)).toBe(0)
+ })
+
+ it("should estimate tokens for text blocks", () => {
+ const content: Array = [
+ { type: "text", text: "This is a text block with 36 characters" },
+ ]
+
+ // With tiktoken, the exact token count may differ from character-based estimation
+ // Instead of expecting an exact number, we verify it's a reasonable positive number
+ const result = estimateTokenCount(content)
+ expect(result).toBeGreaterThan(0)
+
+ // We can also verify that longer text results in more tokens
+ const longerContent: Array = [
+ {
+ type: "text",
+ text: "This is a longer text block with significantly more characters to encode into tokens",
+ },
+ ]
+ const longerResult = estimateTokenCount(longerContent)
+ expect(longerResult).toBeGreaterThan(result)
+ })
+
+ it("should estimate tokens for image blocks based on data size", () => {
+ // Small image
+ const smallImage: Array = [
+ { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "small_dummy_data" } },
+ ]
+ // Larger image with more data
+ const largerImage: Array = [
+ { type: "image", source: { type: "base64", media_type: "image/png", data: "X".repeat(1000) } },
+ ]
+
+ // Verify the token count scales with the size of the image data
+ const smallImageTokens = estimateTokenCount(smallImage)
+ const largerImageTokens = estimateTokenCount(largerImage)
+
+ // Small image should have some tokens
+ expect(smallImageTokens).toBeGreaterThan(0)
+
+ // Larger image should have proportionally more tokens
+ expect(largerImageTokens).toBeGreaterThan(smallImageTokens)
+
+ // Verify the larger image calculation matches our formula including the 50% fudge factor
+ expect(largerImageTokens).toBe(48)
+ })
+
+ it("should estimate tokens for mixed content blocks", () => {
+ const content: Array = [
+ { type: "text", text: "A text block with 30 characters" },
+ { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "dummy_data" } },
+ { type: "text", text: "Another text with 24 chars" },
+ ]
+
+ // We know image tokens calculation should be consistent
+ const imageTokens = Math.ceil(Math.sqrt("dummy_data".length)) * 1.5
+
+ // With tiktoken, we can't predict exact text token counts,
+ // but we can verify the total is greater than just the image tokens
+ const result = estimateTokenCount(content)
+ expect(result).toBeGreaterThan(imageTokens)
+
+ // Also test against a version with only the image to verify text adds tokens
+ const imageOnlyContent: Array = [
+ { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "dummy_data" } },
+ ]
+ const imageOnlyResult = estimateTokenCount(imageOnlyContent)
+ expect(result).toBeGreaterThan(imageOnlyResult)
+ })
+
+ it("should handle empty text blocks", () => {
+ const content: Array = [{ type: "text", text: "" }]
+ expect(estimateTokenCount(content)).toBe(0)
+ })
+
+ it("should handle plain string messages", () => {
+ const content = "This is a plain text message"
+ expect(estimateTokenCount([{ type: "text", text: content }])).toBeGreaterThan(0)
+ })
})
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index 8b646f933b..0fb26ac38f 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -1,5 +1,51 @@
import { Anthropic } from "@anthropic-ai/sdk"
+import { Tiktoken } from "js-tiktoken/lite"
+import o200kBase from "js-tiktoken/ranks/o200k_base"
+
+const TOKEN_FUDGE_FACTOR = 1.5
+
+/**
+ * Counts tokens for user content using tiktoken for text
+ * and a size-based calculation for images.
+ *
+ * @param {Array} content - The content to count tokens for
+ * @returns {number} The token count
+ */
+export function estimateTokenCount(content: Array): number {
+ if (!content || content.length === 0) return 0
+
+ let totalTokens = 0
+ let encoder = null
+
+ // Create encoder
+ encoder = new Tiktoken(o200kBase)
+
+ // Process each content block
+ for (const block of content) {
+ if (block.type === "text") {
+ // Use tiktoken for text token counting
+ const text = block.text || ""
+ if (text.length > 0) {
+ const tokens = encoder.encode(text)
+ totalTokens += tokens.length
+ }
+ } else if (block.type === "image") {
+ // For images, calculate based on data size
+ const imageSource = block.source
+ if (imageSource && typeof imageSource === "object" && "data" in imageSource) {
+ const base64Data = imageSource.data as string
+ totalTokens += Math.ceil(Math.sqrt(base64Data.length))
+ } else {
+ totalTokens += 300 // Conservative estimate for unknown images
+ }
+ }
+ }
+
+ // Add a fudge factor to account for the fact that tiktoken is not always accurate
+ return Math.ceil(totalTokens * TOKEN_FUDGE_FACTOR)
+}
+
/**
* Truncates a conversation by removing a fraction of the messages.
*
@@ -25,10 +71,10 @@ export function truncateConversation(
/**
* Conditionally truncates the conversation messages if the total token count
- * exceeds the model's limit.
+ * exceeds the model's limit, considering the size of incoming content.
*
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
- * @param {number} totalTokens - The total number of tokens in the conversation.
+ * @param {number} totalTokens - The total number of tokens in the conversation (excluding the last user message).
* @param {number} contextWindow - The context window size.
* @param {number} maxTokens - The maximum number of tokens allowed.
* @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
@@ -47,6 +93,22 @@ export function truncateConversationIfNeeded({
contextWindow,
maxTokens,
}: TruncateOptions): Anthropic.Messages.MessageParam[] {
- const allowedTokens = contextWindow - (maxTokens || contextWindow * 0.2)
- return totalTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
+ // Calculate the maximum tokens reserved for response
+ const reservedTokens = maxTokens || contextWindow * 0.2
+
+ // Estimate tokens for the last message (which is always a user message)
+ const lastMessage = messages[messages.length - 1]
+ const lastMessageContent = lastMessage.content
+ const lastMessageTokens = Array.isArray(lastMessageContent)
+ ? estimateTokenCount(lastMessageContent)
+ : estimateTokenCount([{ type: "text", text: lastMessageContent as string }])
+
+ // Calculate total effective tokens (totalTokens never includes the last message)
+ const effectiveTokens = totalTokens + lastMessageTokens
+
+ // Calculate available tokens for conversation history
+ const allowedTokens = contextWindow - reservedTokens
+
+ // Determine if truncation is needed and apply if necessary
+ return effectiveTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
}
From e009d5f14c81bd8b89da288f1d43adfe1e1af544 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Fri, 28 Feb 2025 21:40:47 -0800
Subject: [PATCH 099/298] Fix e2e test paths
---
e2e/.vscode-test.mjs | 2 +-
e2e/package.json | 2 +-
e2e/src/runTest.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/e2e/.vscode-test.mjs b/e2e/.vscode-test.mjs
index 16ea4271ba..ccc8b495ea 100644
--- a/e2e/.vscode-test.mjs
+++ b/e2e/.vscode-test.mjs
@@ -6,7 +6,7 @@ import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
label: 'integrationTest',
- files: 'out/e2e/src/suite/**/*.test.js',
+ files: 'out/suite/**/*.test.js',
workspaceFolder: '.',
mocha: {
ui: 'tdd',
diff --git a/e2e/package.json b/e2e/package.json
index 7e0daac064..630a13f0e7 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -7,7 +7,7 @@
"compile": "tsc -p tsconfig.json",
"lint": "eslint src --ext ts",
"check-types": "tsc --noEmit",
- "test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/e2e/src/runTest.js",
+ "test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/runTest.js",
"ci": "npm run build && npm run test"
},
"dependencies": {},
diff --git a/e2e/src/runTest.ts b/e2e/src/runTest.ts
index 28545efd46..2576c6072f 100644
--- a/e2e/src/runTest.ts
+++ b/e2e/src/runTest.ts
@@ -6,7 +6,7 @@ async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
- const extensionDevelopmentPath = path.resolve(__dirname, "../../../../")
+ const extensionDevelopmentPath = path.resolve(__dirname, "../../")
// The path to the extension test script
// Passed to --extensionTestsPath
From 764d963c104be512187ee9cb600029952c58d0a0 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 08:49:10 -0500
Subject: [PATCH 100/298] Add a 5k token buffer before the end of the context
window
---
.../__tests__/sliding-window.test.ts | 54 +++++++++++++------
src/core/sliding-window/index.ts | 6 ++-
2 files changed, 42 insertions(+), 18 deletions(-)
diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts
index 74b734d738..dbc0c678c2 100644
--- a/src/core/sliding-window/__tests__/sliding-window.test.ts
+++ b/src/core/sliding-window/__tests__/sliding-window.test.ts
@@ -3,7 +3,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "../../../shared/api"
-import { estimateTokenCount, truncateConversation, truncateConversationIfNeeded } from "../index"
+import { TOKEN_BUFFER, estimateTokenCount, truncateConversation, truncateConversationIfNeeded } from "../index"
/**
* Tests for the truncateConversation function
@@ -121,10 +121,10 @@ describe("getMaxTokens", () => {
// Create messages with very small content in the last one to avoid token overflow
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
- // Below max tokens - no truncation
+ // Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 49999,
+ totalTokens: 44999, // Well below threshold + buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -133,7 +133,7 @@ describe("getMaxTokens", () => {
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 50001,
+ totalTokens: 50001, // Above threshold
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -148,10 +148,10 @@ describe("getMaxTokens", () => {
// Create messages with very small content in the last one to avoid token overflow
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
- // Below max tokens - no truncation
+ // Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 79999,
+ totalTokens: 74999, // Well below threshold + buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -160,7 +160,7 @@ describe("getMaxTokens", () => {
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 80001,
+ totalTokens: 80001, // Above threshold
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -175,10 +175,10 @@ describe("getMaxTokens", () => {
// Create messages with very small content in the last one to avoid token overflow
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
- // Below max tokens - no truncation
+ // Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 39999,
+ totalTokens: 34999, // Well below threshold + buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -187,7 +187,7 @@ describe("getMaxTokens", () => {
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 40001,
+ totalTokens: 40001, // Above threshold
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -202,10 +202,10 @@ describe("getMaxTokens", () => {
// Create messages with very small content in the last one to avoid token overflow
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
- // Below max tokens - no truncation
+ // Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 169999,
+ totalTokens: 164999, // Well below threshold + buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -214,7 +214,7 @@ describe("getMaxTokens", () => {
// Above max tokens - truncate
const result2 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 170001,
+ totalTokens: 170001, // Above threshold
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -244,7 +244,7 @@ describe("truncateConversationIfNeeded", () => {
it("should not truncate if tokens are below max tokens threshold", () => {
const modelInfo = createModelInfo(100000, true, 30000)
const maxTokens = 100000 - 30000 // 70000
- const totalTokens = 69999 // Below threshold
+ const totalTokens = 64999 // Well below threshold + buffer
// Create messages with very small content in the last one to avoid token overflow
const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
@@ -337,8 +337,8 @@ describe("truncateConversationIfNeeded", () => {
{ role: messages[messages.length - 1].role, content: smallContent },
]
- // Set base tokens so total is below threshold even with small content added
- const baseTokensForSmall = availableTokens - smallContentTokens - 10
+ // Set base tokens so total is well below threshold + buffer even with small content added
+ const baseTokensForSmall = availableTokens - smallContentTokens - TOKEN_BUFFER - 10
const resultWithSmall = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
totalTokens: baseTokensForSmall,
@@ -388,7 +388,29 @@ describe("truncateConversationIfNeeded", () => {
})
expect(resultWithVeryLarge).not.toEqual(messagesWithVeryLargeContent) // Should truncate
})
+
+ it("should truncate if tokens are within TOKEN_BUFFER of the threshold", () => {
+ const modelInfo = createModelInfo(100000, true, 30000)
+ const maxTokens = 100000 - 30000 // 70000
+ const totalTokens = 66000 // Within 5000 of threshold (70000)
+
+ // Create messages with very small content in the last one to avoid token overflow
+ const messagesWithSmallContent = [...messages.slice(0, -1), { ...messages[messages.length - 1], content: "" }]
+
+ // When truncating, always uses 0.5 fraction
+ // With 4 messages after the first, 0.5 fraction means remove 2 messages
+ const expectedResult = [messagesWithSmallContent[0], messagesWithSmallContent[3], messagesWithSmallContent[4]]
+
+ const result = truncateConversationIfNeeded({
+ messages: messagesWithSmallContent,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ })
+ expect(result).toEqual(expectedResult)
+ })
})
+
/**
* Tests for the estimateTokenCount function
*/
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index 0fb26ac38f..d12e7f337e 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -3,7 +3,8 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Tiktoken } from "js-tiktoken/lite"
import o200kBase from "js-tiktoken/ranks/o200k_base"
-const TOKEN_FUDGE_FACTOR = 1.5
+export const TOKEN_FUDGE_FACTOR = 1.5
+export const TOKEN_BUFFER = 5000
/**
* Counts tokens for user content using tiktoken for text
@@ -110,5 +111,6 @@ export function truncateConversationIfNeeded({
const allowedTokens = contextWindow - reservedTokens
// Determine if truncation is needed and apply if necessary
- return effectiveTokens < allowedTokens ? messages : truncateConversation(messages, 0.5)
+ // Truncate if we're within TOKEN_BUFFER of the limit
+ return effectiveTokens > allowedTokens - TOKEN_BUFFER ? truncateConversation(messages, 0.5) : messages
}
From 997f7bbe8a48c30fc13f7b2808ab93b2e3780316 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Sat, 1 Mar 2025 16:30:46 +0200
Subject: [PATCH 101/298] Improve error handling and messaging in Cline class
and ClineProvider, removed unnecessary resume_task ask, added abandond=true
flag to the abort() call. no more error in log due to resume task
---
src/core/Cline.ts | 42 +++++++++++++------------------
src/core/webview/ClineProvider.ts | 20 ++++++---------
2 files changed, 25 insertions(+), 37 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 158b96ff2c..32a520ca2f 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -374,7 +374,7 @@ export class Cline {
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
if (this.abort) {
- throw new Error("Roo Code instance aborted")
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#1)`)
}
let askTs: number
if (partial !== undefined) {
@@ -392,7 +392,7 @@ export class Cline {
await this.providerRef
.deref()
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
- throw new Error("Current ask promise was ignored 1")
+ throw new Error("Current ask promise was ignored (#1)")
} else {
// this is a new partial message, so add it with partial state
// this.askResponse = undefined
@@ -402,7 +402,7 @@ export class Cline {
this.lastMessageTs = askTs
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
await this.providerRef.deref()?.postStateToWebview()
- throw new Error("Current ask promise was ignored 2")
+ throw new Error("Current ask promise was ignored (#2)")
}
} else {
// partial=false means its a complete version of a previously partial message
@@ -476,7 +476,7 @@ export class Cline {
checkpoint?: Record,
): Promise {
if (this.abort) {
- throw new Error("Roo Code instance aborted")
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#2)`)
}
if (partial !== undefined) {
@@ -568,8 +568,9 @@ export class Cline {
// release this Cline instance from paused state
this.isPaused = false
+ // fake an answer from the subtask that it has completed running and this is the result of what it has done
+ // add the message to the chat history and to the webview ui
try {
- // This adds the completion message to conversation history
await this.say("text", `${lastMessage ?? "Please continue to the next task."}`)
await this.addToApiConversationHistory({
@@ -587,21 +588,6 @@ export class Cline {
?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
throw error
}
-
- try {
- // Resume parent task
- await this.ask("resume_task")
- } catch (error) {
- if (error.message === "Current ask promise was ignored") {
- // ignore the ignored promise, since it was performed by launching a subtask and it probably took more then 1 sec,
- // also set the didAlreadyUseTool flag to indicate that the tool was already used, and there is no need to relaunch it
- this.didAlreadyUseTool = true
- } else {
- // Handle error appropriately
- console.error("Failed to resume task:", error)
- throw error
- }
- }
}
private async resumeTaskFromHistory() {
@@ -1182,7 +1168,7 @@ export class Cline {
async presentAssistantMessage() {
if (this.abort) {
- throw new Error("Roo Code instance aborted")
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#3)`)
}
if (this.presentAssistantMessageLocked) {
@@ -2819,7 +2805,10 @@ export class Cline {
await this.say("completion_result", result, undefined, false)
if (this.isSubTask) {
// tell the provider to remove the current subtask and resume the previous task in the stack
- this.providerRef.deref()?.finishSubTask(lastMessage?.text)
+ await this.providerRef
+ .deref()
+ ?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
+ break
}
}
@@ -2840,7 +2829,10 @@ export class Cline {
await this.say("completion_result", result, undefined, false)
if (this.isSubTask) {
// tell the provider to remove the current subtask and resume the previous task in the stack
- this.providerRef.deref()?.finishSubTask(lastMessage?.text)
+ await this.providerRef
+ .deref()
+ ?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
+ break
}
}
@@ -2936,7 +2928,7 @@ export class Cline {
includeFileDetails: boolean = false,
): Promise {
if (this.abort) {
- throw new Error("Roo Code instance aborted")
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#4)`)
}
if (this.consecutiveMistakeCount >= 3) {
@@ -3178,7 +3170,7 @@ export class Cline {
// need to call here in case the stream was aborted
if (this.abort || this.abandoned) {
- throw new Error("Roo Code instance aborted")
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#5)`)
}
this.didCompleteReadingStream = true
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index f92650f887..3cd01d275a 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -152,7 +152,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const removedTaskNumber = clineToBeRemoved.getTaskNumber()
try {
- await clineToBeRemoved.abortTask()
+ // abort the running task and set isAbandoned to true so all running promises will exit as well
+ await clineToBeRemoved.abortTask(true)
} catch (abortError) {
this.log(`Error failed aborting task ${removedTaskNumber}: ${abortError.message}`)
}
@@ -223,7 +224,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// remove the last cline instance from the stack (this is the finished sub task)
await this.removeClineFromStack()
// resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
- this.getCurrentCline()?.resumePausedTask(`new_task finished successfully! ${lastMessage}`)
+ this.getCurrentCline()?.resumePausedTask(lastMessage)
} catch (error) {
this.log(`Error in finishSubTask: ${error.message}`)
throw error
@@ -923,13 +924,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
break
case "clearTask":
- // newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
- await this.removeClineFromStack()
- // resume previouse task with subtask failed error
- this.getCurrentCline()?.resumePausedTask(
+ // clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed
+ await this.finishSubTask(
`new_task finished with an error!, it was stopped and canceled by the user.`,
)
-
await this.postStateToWebview()
break
case "didShowAnnouncement":
@@ -2087,11 +2085,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// remove task from stack if it's the current task
if (id === this.getCurrentCline()?.taskId) {
- await this.removeClineWithIdFromStack(id)
- // resume previouse task with subtask failed error
- this.getCurrentCline()?.resumePausedTask(
- `new_task finished with an error!, it was stopped and delted by the user.`,
- )
+ // if we found the taskid to delete - call finish to abort this task and allow a new task to be started,
+ // if we are deleting a subtask and parent task is still waiting for subtask to finish - it allows the parent to resume (this case should neve exist)
+ await this.finishSubTask(`new_task finished with an error!, it was stopped and delted by the user.`)
}
// delete task from the task history state
From 251b83629aba03727ce12a598e1bbad84eafa678 Mon Sep 17 00:00:00 2001
From: aheizi
Date: Fri, 28 Feb 2025 21:52:48 +0800
Subject: [PATCH 102/298] feat: add shortcut to switch modes
---
.../src/components/chat/ChatTextArea.tsx | 7 ++++
webview-ui/src/components/chat/ChatView.tsx | 32 +++++++++++++++++++
.../chat/__tests__/ChatTextArea.test.tsx | 1 +
3 files changed, 40 insertions(+)
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index dcbe085147..a2af6a3a71 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -31,6 +31,7 @@ interface ChatTextAreaProps {
onHeightChange?: (height: number) => void
mode: Mode
setMode: (value: Mode) => void
+ modeShortcutText: string
}
const ChatTextArea = forwardRef(
@@ -48,6 +49,7 @@ const ChatTextArea = forwardRef(
onHeightChange,
mode,
setMode,
+ modeShortcutText,
},
ref,
) => {
@@ -816,6 +818,11 @@ const ChatTextArea = forwardRef(
minWidth: "70px",
flex: "0 0 auto",
}}>
+
{getAllModes(customModes).map((mode) => (