From 048863e03991a3b49429a77827da2f96b32889f9 Mon Sep 17 00:00:00 2001
From: Piotr Rogowski
Date: Sun, 26 Jan 2025 08:31:07 +0100
Subject: [PATCH 001/541] Do not exclude whole project dir when listing in case
where project is places inside excluded dir (like /tmp or ~/tmp)
---
src/services/glob/list-files.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts
index 8578b914d72..c7e3d41cf03 100644
--- a/src/services/glob/list-files.ts
+++ b/src/services/glob/list-files.ts
@@ -34,7 +34,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
"pkg",
"Pods",
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
- ].map((dir) => `**/${dir}/**`)
+ ].map((dir) => `${dirPath}/**/${dir}/**`)
const options = {
cwd: dirPath,
From 1cd90a655b0db197b6dd248e854eef366d148849 Mon Sep 17 00:00:00 2001
From: kohii
Date: Sun, 2 Feb 2025 10:17:57 +0900
Subject: [PATCH 002/541] feat: Add Kotlin support in
list_code_definition_names
---
esbuild.js | 1 +
.../tree-sitter/__tests__/index.test.ts | 6 ++++
.../__tests__/languageParser.test.ts | 11 ++++++++
src/services/tree-sitter/index.ts | 3 ++
src/services/tree-sitter/languageParser.ts | 6 ++++
src/services/tree-sitter/queries/index.ts | 1 +
src/services/tree-sitter/queries/kotlin.ts | 28 +++++++++++++++++++
7 files changed, 56 insertions(+)
create mode 100644 src/services/tree-sitter/queries/kotlin.ts
diff --git a/esbuild.js b/esbuild.js
index 8b203076e45..7907dd1c390 100644
--- a/esbuild.js
+++ b/esbuild.js
@@ -52,6 +52,7 @@ const copyWasmFiles = {
"java",
"php",
"swift",
+ "kotlin",
]
languages.forEach((lang) => {
diff --git a/src/services/tree-sitter/__tests__/index.test.ts b/src/services/tree-sitter/__tests__/index.test.ts
index 4a5782dcb1e..8372e7e5808 100644
--- a/src/services/tree-sitter/__tests__/index.test.ts
+++ b/src/services/tree-sitter/__tests__/index.test.ts
@@ -169,6 +169,8 @@ describe("Tree-sitter Service", () => {
"/test/path/main.rs",
"/test/path/program.cpp",
"/test/path/code.go",
+ "/test/path/app.kt",
+ "/test/path/script.kts",
]
;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()])
@@ -197,6 +199,8 @@ describe("Tree-sitter Service", () => {
rs: { parser: mockParser, query: mockQuery },
cpp: { parser: mockParser, query: mockQuery },
go: { parser: mockParser, query: mockQuery },
+ kt: { parser: mockParser, query: mockQuery },
+ kts: { parser: mockParser, query: mockQuery },
})
;(fs.readFile as jest.Mock).mockResolvedValue("function test() {}")
@@ -207,6 +211,8 @@ describe("Tree-sitter Service", () => {
expect(result).toContain("main.rs")
expect(result).toContain("program.cpp")
expect(result).toContain("code.go")
+ expect(result).toContain("app.kt")
+ expect(result).toContain("script.kts")
})
it("should normalize paths in output", async () => {
diff --git a/src/services/tree-sitter/__tests__/languageParser.test.ts b/src/services/tree-sitter/__tests__/languageParser.test.ts
index 1b92d81b6be..54271e30e87 100644
--- a/src/services/tree-sitter/__tests__/languageParser.test.ts
+++ b/src/services/tree-sitter/__tests__/languageParser.test.ts
@@ -92,6 +92,17 @@ describe("Language Parser", () => {
expect(parsers.hpp).toBeDefined()
})
+ it("should handle Kotlin files correctly", async () => {
+ const files = ["test.kt", "test.kts"]
+ const parsers = await loadRequiredLanguageParsers(files)
+
+ expect(ParserMock.Language.load).toHaveBeenCalledWith(expect.stringContaining("tree-sitter-kotlin.wasm"))
+ expect(parsers.kt).toBeDefined()
+ expect(parsers.kts).toBeDefined()
+ expect(parsers.kt.query).toBeDefined()
+ expect(parsers.kts.query).toBeDefined()
+ })
+
it("should throw error for unsupported file extensions", async () => {
const files = ["test.unsupported"]
diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts
index 83e02ac6158..5b48da885dc 100644
--- a/src/services/tree-sitter/index.ts
+++ b/src/services/tree-sitter/index.ts
@@ -73,6 +73,9 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
"java",
"php",
"swift",
+ // Kotlin
+ "kt",
+ "kts",
].map((e) => `.${e}`)
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts
index 2d791b39a8d..f256b0b62ad 100644
--- a/src/services/tree-sitter/languageParser.ts
+++ b/src/services/tree-sitter/languageParser.ts
@@ -13,6 +13,7 @@ import {
javaQuery,
phpQuery,
swiftQuery,
+ kotlinQuery,
} from "./queries"
export interface LanguageParser {
@@ -120,6 +121,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi
language = await loadLanguage("swift")
query = language.query(swiftQuery)
break
+ case "kt":
+ case "kts":
+ language = await loadLanguage("kotlin")
+ query = language.query(kotlinQuery)
+ break
default:
throw new Error(`Unsupported language: ${ext}`)
}
diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts
index 889210a8e58..818eacca01e 100644
--- a/src/services/tree-sitter/queries/index.ts
+++ b/src/services/tree-sitter/queries/index.ts
@@ -10,3 +10,4 @@ export { default as cQuery } from "./c"
export { default as csharpQuery } from "./c-sharp"
export { default as goQuery } from "./go"
export { default as swiftQuery } from "./swift"
+export { default as kotlinQuery } from "./kotlin"
diff --git a/src/services/tree-sitter/queries/kotlin.ts b/src/services/tree-sitter/queries/kotlin.ts
new file mode 100644
index 00000000000..61eb112448b
--- /dev/null
+++ b/src/services/tree-sitter/queries/kotlin.ts
@@ -0,0 +1,28 @@
+/*
+- class declarations (including interfaces)
+- function declarations
+- object declarations
+- property declarations
+- type alias declarations
+*/
+export default `
+(class_declaration
+ (type_identifier) @name.definition.class
+) @definition.class
+
+(function_declaration
+ (simple_identifier) @name.definition.function
+) @definition.function
+
+(object_declaration
+ (type_identifier) @name.definition.object
+) @definition.object
+
+(property_declaration
+ (simple_identifier) @name.definition.property
+) @definition.property
+
+(type_alias
+ (type_identifier) @name.definition.type
+) @definition.type
+`
From fbf65bfc6c1eeef107e9aa27245ea7bd64a5adf7 Mon Sep 17 00:00:00 2001
From: axb
Date: Wed, 12 Feb 2025 17:32:55 +0800
Subject: [PATCH 003/541] Reduce the probability of errors when the model tries
to fix the problem due to mismatched line numbers after applying diff
---
src/core/mentions/index.ts | 4 ++--
src/integrations/diagnostics/index.ts | 10 +++++++---
src/integrations/editor/DiffViewProvider.ts | 2 +-
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index cf5bdeaae01..cf87241f23a 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -186,9 +186,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
}
}
-function getWorkspaceProblems(cwd: string): string {
+async function getWorkspaceProblems(cwd: string): Promise {
const diagnostics = vscode.languages.getDiagnostics()
- const result = diagnosticsToProblemsString(
+ const result = await diagnosticsToProblemsString(
diagnostics,
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
cwd,
diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts
index ad4ee7755cd..2d829f26e76 100644
--- a/src/integrations/diagnostics/index.ts
+++ b/src/integrations/diagnostics/index.ts
@@ -70,11 +70,12 @@ export function getNewDiagnostics(
// // - New error in file3 (1:1)
// will return empty string if no problems with the given severity are found
-export function diagnosticsToProblemsString(
+export async function diagnosticsToProblemsString(
diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
severities: vscode.DiagnosticSeverity[],
cwd: string,
-): string {
+): Promise {
+ const documents = new Map()
let result = ""
for (const [uri, fileDiagnostics] of diagnostics) {
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
@@ -100,7 +101,10 @@ export function diagnosticsToProblemsString(
}
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
const source = diagnostic.source ? `${diagnostic.source} ` : ""
- result += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`
+ const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri))
+ documents.set(uri, document)
+ const lineContent = document.lineAt(diagnostic.range.start.line).text
+ result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}`
}
}
}
diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts
index ee24d7db4ee..8f7e387c7a2 100644
--- a/src/integrations/editor/DiffViewProvider.ts
+++ b/src/integrations/editor/DiffViewProvider.ts
@@ -172,7 +172,7 @@ export class DiffViewProvider {
initial fix is usually correct and it may just take time for linters to catch up.
*/
const postDiagnostics = vscode.languages.getDiagnostics()
- const newProblems = diagnosticsToProblemsString(
+ const newProblems = await diagnosticsToProblemsString(
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
[
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
From c3c3874593bbd00e6411c566d0875c700994a061 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Sat, 22 Feb 2025 23:52:13 +0200
Subject: [PATCH 004/541] subtasks alpha version (still in development)
---
src/activate/registerCommands.ts | 2 +-
src/core/Cline.ts | 79 ++++++-
src/core/webview/ClineProvider.ts | 211 ++++++++++++------
.../webview/__tests__/ClineProvider.test.ts | 2 +-
src/exports/index.ts | 2 +-
5 files changed, 218 insertions(+), 78 deletions(-)
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 69e257e7a51..79f57b25095 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -20,7 +20,7 @@ export const registerCommands = (options: RegisterCommandOptions) => {
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
return {
"roo-cline.plusButtonClicked": async () => {
- await provider.clearTask()
+ await provider.removeClineFromStack()
await provider.postStateToWebview()
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
},
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 9c2977a2669..134961129cc 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -75,6 +75,10 @@ type UserContent = Array<
export class Cline {
readonly taskId: string
+ // a flag that indicated if this Cline instance is a subtask (on finish return control to parent task)
+ 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
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -160,6 +164,12 @@ export class Cline {
}
}
+ // a helper function to set the private member isSubTask to true
+ // and by that set this Cline instance to be a subtask (on finish return control to parent task)
+ setSubTask() {
+ this.isSubTask = true
+ }
+
// Add method to update diffStrategy
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
// If not provided, get from current state
@@ -480,6 +490,43 @@ export class Cline {
])
}
+ async resumePausedTask() {
+ // release this Cline instance from paused state
+ this.isPaused = false
+
+ // Clear any existing ask state and simulate a completed ask response
+ // this.askResponse = "messageResponse";
+ // this.askResponseText = "Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.";
+ // this.askResponseImages = undefined;
+ // this.lastMessageTs = Date.now();
+
+ // This adds the completion message to conversation history
+ await this.say(
+ "text",
+ "Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.",
+ )
+
+ // this.userMessageContent.push({
+ // type: "text",
+ // text: `${"Result:\\n\\nSub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task."}`,
+ // })
+
+ 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() {
const modifiedClineMessages = await this.getSavedClineMessages()
@@ -2553,10 +2600,12 @@ export class Cline {
const provider = this.providerRef.deref()
if (provider) {
await provider.handleModeSwitch(mode)
- await provider.initClineWithTask(message)
+ 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
+ this.isPaused = true
} else {
pushToolResult(
formatResponse.toolError("Failed to create new task: provider not available"),
@@ -2648,6 +2697,10 @@ export class Cline {
if (lastMessage && lastMessage.ask !== "command") {
// havent sent a command message yet so first send completion_result then command
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()
+ }
}
// complete command message
@@ -2665,6 +2718,10 @@ export class Cline {
commandResult = execCommandResult
} else {
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()
+ }
}
// we already sent completion_result says, an empty string asks relinquishes control over button and field
@@ -2740,6 +2797,20 @@ export class Cline {
}
}
+ // this function checks if this Cline instance is set to pause state and wait for being resumed,
+ // this is used when a sub-task is launched and the parent task is waiting for it to finish
+ async waitForResume() {
+ // wait until isPaused is false
+ await new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (!this.isPaused) {
+ clearInterval(interval)
+ resolve()
+ }
+ }, 1000) // TBD: the 1 sec should be added to the settings, also should add a timeout to prevent infinit wait
+ })
+ }
+
async recursivelyMakeClineRequests(
userContent: UserContent,
includeFileDetails: boolean = false,
@@ -2779,6 +2850,12 @@ export class Cline {
await this.checkpointSave({ isFirst: true })
}
+ // 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) {
+ await this.waitForResume()
+ }
+
// 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
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
await this.say(
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 05faa138342..d690efa9dd5 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -147,7 +147,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private disposables: vscode.Disposable[] = []
private view?: vscode.WebviewView | vscode.WebviewPanel
private isViewLaunched = false
- private cline?: Cline
+ private clineStack: Cline[] = []
private workspaceTracker?: WorkspaceTracker
protected mcpHub?: McpHub // Change from private to protected
private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
@@ -176,6 +176,55 @@ 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 {
+ this.clineStack.push(cline)
+ }
+
+ // 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) {
+ await clineToBeRemoved.abortTask()
+ // make sure no reference kept, once promises end it will be garbage collected
+ clineToBeRemoved = undefined
+ }
+ }
+
+ // 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()
+ }
+ }
+
+ // returns the current cline object in the stack (the top one)
+ // if the stack is empty, returns undefined
+ getCurrentCline(): Cline | undefined {
+ if (this.clineStack.length === 0) {
+ return undefined
+ }
+ return this.clineStack[this.clineStack.length - 1]
+ }
+
+ // remove the current task/cline instance (at the top of the stack), ao this task is finished
+ // 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() {
+ // 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()
+ }
+
/*
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -183,7 +232,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
*/
async dispose() {
this.outputChannel.appendLine("Disposing ClineProvider...")
- await this.clearTask()
+ await this.removeClineFromStack()
this.outputChannel.appendLine("Cleared task")
if (this.view && "dispose" in this.view) {
this.view.dispose()
@@ -236,7 +285,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return false
}
- if (visibleProvider.cline) {
+ // check if there is a cline instance in the stack (if this provider has an active task)
+ if (visibleProvider.getCurrentCline()) {
return true
}
@@ -267,7 +317,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return
}
- if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
+ if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
await visibleProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
@@ -303,7 +353,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return
}
- if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
+ if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
await visibleProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
@@ -392,13 +442,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
// if the extension is starting a new session, clear previous task state
- this.clearTask()
+ await this.removeClineFromStack()
this.outputChannel.appendLine("Webview view resolved")
}
+ // a wrapper that inits a new Cline instance (Task) ans setting it as a sub task of the current task
+ public async initClineWithSubTask(task?: string, images?: string[]) {
+ await this.initClineWithTask(task, images)
+ this.getCurrentCline()?.setSubTask()
+ }
+
+ // when initializing a new task, (not from history but from a tool command new_task) there is no need to remove the previouse task
+ // since the new task is a sub task of the previous one, and when it finishes it is removed from the stack and the caller is resumed
+ // in this way we can have a chain of tasks, each one being a sub task of the previous one until the main task is finished
public async initClineWithTask(task?: string, images?: string[]) {
- await this.clearTask()
const {
apiConfiguration,
customModePrompts,
@@ -413,7 +471,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
- this.cline = new Cline(
+ const newCline = new Cline(
this,
apiConfiguration,
effectiveInstructions,
@@ -425,10 +483,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
undefined,
experiments,
)
+ this.addClineToStack(newCline)
}
public async initClineWithHistoryItem(historyItem: HistoryItem) {
- await this.clearTask()
+ await this.removeClineFromStack()
const {
apiConfiguration,
@@ -444,7 +503,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
- this.cline = new Cline(
+ const newCline = new Cline(
this,
apiConfiguration,
effectiveInstructions,
@@ -456,6 +515,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
historyItem,
experiments,
)
+ this.addClineToStack(newCline)
}
public async postMessageToWebview(message: ExtensionMessage) {
@@ -810,11 +870,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
break
case "askResponse":
- this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
+ this.getCurrentCline()?.handleWebviewAskResponse(
+ message.askResponse!,
+ message.text,
+ message.images,
+ )
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.clearTask()
+ await this.removeClineFromStack()
await this.postStateToWebview()
break
case "didShowAnnouncement":
@@ -826,7 +890,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postMessageToWebview({ type: "selectedImages", images })
break
case "exportCurrentTask":
- const currentTaskId = this.cline?.taskId
+ const currentTaskId = this.getCurrentCline()?.taskId
if (currentTaskId) {
this.exportTaskWithId(currentTaskId)
}
@@ -892,7 +956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
const result = checkoutDiffPayloadSchema.safeParse(message.payload)
if (result.success) {
- await this.cline?.checkpointDiff(result.data)
+ await this.getCurrentCline()?.checkpointDiff(result.data)
}
break
@@ -903,13 +967,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.cancelTask()
try {
- await pWaitFor(() => this.cline?.isInitialized === true, { timeout: 3_000 })
+ await pWaitFor(() => this.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })
} catch (error) {
vscode.window.showErrorMessage("Timed out when attempting to restore checkpoint.")
}
try {
- await this.cline?.checkpointRestore(result.data)
+ await this.getCurrentCline()?.checkpointRestore(result.data)
} catch (error) {
vscode.window.showErrorMessage("Failed to restore checkpoint.")
}
@@ -1145,42 +1209,43 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
if (
(answer === "Just this message" || answer === "This and all subsequent messages") &&
- this.cline &&
+ this.getCurrentCline() &&
typeof message.value === "number" &&
message.value
) {
const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
- const messageIndex = this.cline.clineMessages.findIndex(
- (msg) => msg.ts && msg.ts >= timeCutoff,
- )
- const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(
+ const messageIndex = this.getCurrentCline()!.clineMessages.findIndex(
(msg) => msg.ts && msg.ts >= timeCutoff,
)
+ const apiConversationHistoryIndex =
+ this.getCurrentCline()?.apiConversationHistory.findIndex(
+ (msg) => msg.ts && msg.ts >= timeCutoff,
+ )
if (messageIndex !== -1) {
- const { historyItem } = await this.getTaskWithId(this.cline.taskId)
+ const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
if (answer === "Just this message") {
// Find the next user message first
- const nextUserMessage = this.cline.clineMessages
- .slice(messageIndex + 1)
+ const nextUserMessage = this.getCurrentCline()!
+ .clineMessages.slice(messageIndex + 1)
.find((msg) => msg.type === "say" && msg.say === "user_feedback")
// Handle UI messages
if (nextUserMessage) {
// Find absolute index of next user message
- const nextUserMessageIndex = this.cline.clineMessages.findIndex(
+ const nextUserMessageIndex = this.getCurrentCline()!.clineMessages.findIndex(
(msg) => msg === nextUserMessage,
)
// Keep messages before current message and after next user message
- await this.cline.overwriteClineMessages([
- ...this.cline.clineMessages.slice(0, messageIndex),
- ...this.cline.clineMessages.slice(nextUserMessageIndex),
+ await this.getCurrentCline()!.overwriteClineMessages([
+ ...this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
+ ...this.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
])
} else {
// If no next user message, keep only messages before current message
- await this.cline.overwriteClineMessages(
- this.cline.clineMessages.slice(0, messageIndex),
+ await this.getCurrentCline()!.overwriteClineMessages(
+ this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
)
}
@@ -1188,30 +1253,36 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (apiConversationHistoryIndex !== -1) {
if (nextUserMessage && nextUserMessage.ts) {
// Keep messages before current API message and after next user message
- await this.cline.overwriteApiConversationHistory([
- ...this.cline.apiConversationHistory.slice(
+ await this.getCurrentCline()!.overwriteApiConversationHistory([
+ ...this.getCurrentCline()!.apiConversationHistory.slice(
0,
apiConversationHistoryIndex,
),
- ...this.cline.apiConversationHistory.filter(
+ ...this.getCurrentCline()!.apiConversationHistory.filter(
(msg) => msg.ts && msg.ts >= nextUserMessage.ts,
),
])
} else {
// If no next user message, keep only messages before current API message
- await this.cline.overwriteApiConversationHistory(
- this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
+ await this.getCurrentCline()!.overwriteApiConversationHistory(
+ this.getCurrentCline()!.apiConversationHistory.slice(
+ 0,
+ apiConversationHistoryIndex,
+ ),
)
}
}
} else if (answer === "This and all subsequent messages") {
// Delete this message and all that follow
- await this.cline.overwriteClineMessages(
- this.cline.clineMessages.slice(0, messageIndex),
+ await this.getCurrentCline()!.overwriteClineMessages(
+ this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
)
if (apiConversationHistoryIndex !== -1) {
- await this.cline.overwriteApiConversationHistory(
- this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
+ await this.getCurrentCline()!.overwriteApiConversationHistory(
+ this.getCurrentCline()!.apiConversationHistory.slice(
+ 0,
+ apiConversationHistoryIndex,
+ ),
)
}
}
@@ -1481,8 +1552,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("experiments", updatedExperiments)
// Update diffStrategy in current Cline instance if it exists
- if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.cline) {
- await this.cline.updateDiffStrategy(
+ if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.getCurrentCline()) {
+ await this.getCurrentCline()!.updateDiffStrategy(
Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.DIFF_STRATEGY),
)
}
@@ -1724,25 +1795,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
this.updateGlobalState("modelTemperature", modelTemperature),
])
- if (this.cline) {
- this.cline.api = buildApiHandler(apiConfiguration)
+ if (this.getCurrentCline()) {
+ this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
}
}
async cancelTask() {
- if (this.cline) {
- const { historyItem } = await this.getTaskWithId(this.cline.taskId)
- this.cline.abortTask()
+ if (this.getCurrentCline()) {
+ const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
+ this.getCurrentCline()!.abortTask()
await pWaitFor(
() =>
- this.cline === undefined ||
- this.cline.isStreaming === false ||
- this.cline.didFinishAbortingStream ||
+ this.getCurrentCline()! === undefined ||
+ this.getCurrentCline()!.isStreaming === false ||
+ this.getCurrentCline()!.didFinishAbortingStream ||
// If only the first chunk is processed, then there's no
// need to wait for graceful abort (closes edits, browser,
// etc).
- this.cline.isWaitingForFirstChunk,
+ this.getCurrentCline()!.isWaitingForFirstChunk,
{
timeout: 3_000,
},
@@ -1750,11 +1821,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
console.error("Failed to abort task")
})
- if (this.cline) {
+ if (this.getCurrentCline()) {
// 'abandoned' will prevent this Cline instance from affecting
// future Cline instances. This may happen if its hanging on a
// streaming request.
- this.cline.abandoned = true
+ this.getCurrentCline()!.abandoned = true
}
// Clears task again, so we need to abortTask manually above.
@@ -1765,8 +1836,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field
await this.updateGlobalState("customInstructions", instructions || undefined)
- if (this.cline) {
- this.cline.customInstructions = instructions || undefined
+ if (this.getCurrentCline()) {
+ this.getCurrentCline()!.customInstructions = instructions || undefined
}
await this.postStateToWebview()
}
@@ -1980,8 +2051,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("apiProvider", openrouter)
await this.storeSecret("openRouterApiKey", apiKey)
await this.postStateToWebview()
- if (this.cline) {
- this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
+ if (this.getCurrentCline()) {
+ this.getCurrentCline()!.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
@@ -2012,8 +2083,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("apiProvider", glama)
await this.storeSecret("glamaApiKey", apiKey)
await this.postStateToWebview()
- if (this.cline) {
- this.cline.api = buildApiHandler({
+ if (this.getCurrentCline()) {
+ this.getCurrentCline()!.api = buildApiHandler({
apiProvider: glama,
glamaApiKey: apiKey,
})
@@ -2295,7 +2366,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async showTaskWithId(id: string) {
- if (id !== this.cline?.taskId) {
+ if (id !== this.getCurrentCline()?.taskId) {
// non-current task
const { historyItem } = await this.getTaskWithId(id)
await this.initClineWithHistoryItem(historyItem) // clears existing task
@@ -2309,8 +2380,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async deleteTaskWithId(id: string) {
- if (id === this.cline?.taskId) {
- await this.clearTask()
+ if (id === this.getCurrentCline()?.taskId) {
+ await this.removeClineWithIdFromStack(id)
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
@@ -2434,10 +2505,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowMcp: alwaysAllowMcp ?? false,
alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
uriScheme: vscode.env.uriScheme,
- currentTaskItem: this.cline?.taskId
- ? (taskHistory || []).find((item) => item.id === this.cline?.taskId)
+ currentTaskItem: this.getCurrentCline()?.taskId
+ ? (taskHistory || []).find((item) => item.id === this.getCurrentCline()?.taskId)
: undefined,
- clineMessages: this.cline?.clineMessages || [],
+ clineMessages: this.getCurrentCline()?.clineMessages || [],
taskHistory: (taskHistory || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
@@ -2472,11 +2543,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- async clearTask() {
- this.cline?.abortTask()
- this.cline = undefined // removes reference to it, so once promises end it will be garbage collected
- }
-
// Caching mechanism to keep track of webview messages + API conversation history per provider instance
/*
@@ -2914,10 +2980,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
await this.configManager.resetAllConfigs()
await this.customModesManager.resetCustomModes()
- if (this.cline) {
- this.cline.abortTask()
- this.cline = undefined
- }
+ await this.removeClineFromStack()
await this.postStateToWebview()
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
}
@@ -2935,7 +2998,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
get messages() {
- return this.cline?.clineMessages || []
+ return this.getCurrentCline()?.clineMessages || []
}
// Add public getter
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 0a8f73308f5..15d0eff7086 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -381,7 +381,7 @@ describe("ClineProvider", () => {
// @ts-ignore - accessing private property for testing
provider.cline = { abortTask: mockAbortTask }
- await provider.clearTask()
+ await provider.removeClineFromStack()
expect(mockAbortTask).toHaveBeenCalled()
// @ts-ignore - accessing private property for testing
diff --git a/src/exports/index.ts b/src/exports/index.ts
index a0680b04829..e4b17da4844 100644
--- a/src/exports/index.ts
+++ b/src/exports/index.ts
@@ -15,7 +15,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
- await sidebarProvider.clearTask()
+ await sidebarProvider.removeClineFromStack()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
await sidebarProvider.postMessageToWebview({
From 87f6ac4d06083a895aadc5c15ffb1532eb70ba9e Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Sun, 23 Feb 2025 05:15:23 +0200
Subject: [PATCH 005/541] pass last message of a subtask to parent task
---
src/core/Cline.ts | 32 ++++++++++++++-----------------
src/core/webview/ClineProvider.ts | 4 ++--
2 files changed, 16 insertions(+), 20 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 134961129cc..54de357d469 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -490,26 +490,22 @@ export class Cline {
])
}
- async resumePausedTask() {
+ async resumePausedTask(lastMessage?: string) {
// release this Cline instance from paused state
this.isPaused = false
- // Clear any existing ask state and simulate a completed ask response
- // this.askResponse = "messageResponse";
- // this.askResponseText = "Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.";
- // this.askResponseImages = undefined;
- // this.lastMessageTs = Date.now();
-
// This adds the completion message to conversation history
- await this.say(
- "text",
- "Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.",
- )
-
- // this.userMessageContent.push({
- // type: "text",
- // text: `${"Result:\\n\\nSub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task."}`,
- // })
+ await this.say("text", `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`)
+
+ await this.addToApiConversationHistory({
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`,
+ },
+ ],
+ })
try {
// Resume parent task
@@ -2699,7 +2695,7 @@ 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()
+ this.providerRef.deref()?.finishSubTask(lastMessage?.text)
}
}
@@ -2720,7 +2716,7 @@ 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()
+ this.providerRef.deref()?.finishSubTask(lastMessage?.text)
}
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index d690efa9dd5..b00c744ff66 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -218,11 +218,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// remove the current task/cline instance (at the top of the stack), ao this task is finished
// 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() {
+ 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()
+ this.getCurrentCline()?.resumePausedTask(lastMessage)
}
/*
From 20f90732043223163275ba1c69d406d03a3ba22f Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 23 Feb 2025 02:20:36 -0500
Subject: [PATCH 006/541] Change response to be a user message
---
src/core/Cline.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 54de357d469..35879a42913 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -498,11 +498,11 @@ export class Cline {
await this.say("text", `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`)
await this.addToApiConversationHistory({
- role: "assistant",
+ role: "user",
content: [
{
type: "text",
- text: `new_task finished successfully! ${lastMessage ?? "Please continue to the next task."}`,
+ text: `[new_task completed] Result: ${lastMessage ?? "Please continue to the next task."}`,
},
],
})
From 655930cf5f3925460414e9098d8851549e4a3333 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Mon, 24 Feb 2025 00:27:40 +0200
Subject: [PATCH 007/541] added getClineStackSize() to ClineProvider and fixed
its tests
---
src/core/webview/ClineProvider.ts | 5 ++
.../webview/__tests__/ClineProvider.test.ts | 57 +++++++++++++------
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index b00c744ff66..a8f10f6fcd0 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -215,6 +215,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return this.clineStack[this.clineStack.length - 1]
}
+ // returns the current clineStack length (how many cline objects are in the stack)
+ getClineStackSize(): number {
+ return this.clineStack.length
+ }
+
// remove the current task/cline instance (at the top of the stack), ao this task is finished
// 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
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 15d0eff7086..65408b8973b 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -8,6 +8,7 @@ import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessa
import { setSoundEnabled } from "../../../utils/sound"
import { defaultModeSlug } from "../../../shared/modes"
import { experimentDefault } from "../../../shared/experiments"
+import { Cline } from "../../Cline"
// Mock custom-instructions module
const mockAddCustomInstructions = jest.fn()
@@ -377,15 +378,43 @@ describe("ClineProvider", () => {
})
test("clearTask aborts current task", async () => {
+ // prepare the mock object
const mockAbortTask = jest.fn()
- // @ts-ignore - accessing private property for testing
- provider.cline = { abortTask: mockAbortTask }
+ const clineMock = { abortTask: mockAbortTask } as unknown as Cline
+
+ // add the mock object to the stack
+ provider.addClineToStack(clineMock)
+ // get the stack size before the abort call
+ const stackSizeBeforeAbort = provider.getClineStackSize()
+
+ // call the removeClineFromStack method so it will call the current cline abort and remove it from the stack
await provider.removeClineFromStack()
+ // get the stack size after the abort call
+ const stackSizeAfterAbort = provider.getClineStackSize()
+
+ // check if the abort method was called
expect(mockAbortTask).toHaveBeenCalled()
- // @ts-ignore - accessing private property for testing
- expect(provider.cline).toBeUndefined()
+
+ // 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" } as unknown as Cline
+ const mockCline2 = { taskId: "test-task-id-2" } as unknown as Cline
+
+ // add Cline instances to the stack
+ provider.addClineToStack(mockCline1)
+ provider.addClineToStack(mockCline2)
+
+ // verify cline instances were added to the stack
+ expect(provider.getClineStackSize()).toBe(2)
+
+ // verify current cline instance is the last one added
+ expect(provider.getCurrentCline()).toBe(mockCline2)
})
test("getState returns correct initial state", async () => {
@@ -788,9 +817,8 @@ describe("ClineProvider", () => {
taskId: "test-task-id",
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
+ } as unknown as Cline
+ provider.addClineToStack(mockCline)
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -841,9 +869,8 @@ describe("ClineProvider", () => {
taskId: "test-task-id",
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
+ } as unknown as Cline
+ provider.addClineToStack(mockCline)
// Mock getTaskWithId
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
@@ -871,9 +898,8 @@ describe("ClineProvider", () => {
overwriteClineMessages: jest.fn(),
overwriteApiConversationHistory: jest.fn(),
taskId: "test-task-id",
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
+ } as unknown as Cline
+ provider.addClineToStack(mockCline)
// Trigger message deletion
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -1377,9 +1403,8 @@ describe("ClineProvider", () => {
const mockCline = {
api: undefined,
abortTask: jest.fn(),
- }
- // @ts-ignore - accessing private property for testing
- provider.cline = mockCline
+ } as unknown as Cline
+ provider.addClineToStack(mockCline)
const testApiConfig = {
apiProvider: "anthropic" as const,
From 01765995ada92491ef7576d10f8d7a8da22f4223 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Mon, 24 Feb 2025 23:00:11 +0200
Subject: [PATCH 008/541] added task no indicator + improved deleteTask code
---
src/core/Cline.ts | 13 ++++
src/core/__tests__/Cline.test.ts | 1 +
src/core/webview/ClineProvider.ts | 72 +++++++++----------
src/shared/HistoryItem.ts | 1 +
webview-ui/src/components/chat/TaskHeader.tsx | 5 +-
.../src/components/history/HistoryPreview.tsx | 6 ++
.../history/__tests__/HistoryView.test.tsx | 2 +
7 files changed, 62 insertions(+), 38 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 35879a42913..bcec6243fff 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -75,6 +75,7 @@ type UserContent = Array<
export class Cline {
readonly taskId: string
+ private taskNumber: number
// a flag that indicated if this Cline instance is a subtask (on finish return control to parent task)
private isSubTask: boolean = false
// a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
@@ -139,6 +140,7 @@ export class Cline {
}
this.taskId = crypto.randomUUID()
+ this.taskNumber = -1
this.api = buildApiHandler(apiConfiguration)
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
@@ -170,6 +172,16 @@ export class Cline {
this.isSubTask = true
}
+ // sets the task number (sequencial number of this task from all the subtask ran from this main task stack)
+ setTaskNumber(taskNumber: number) {
+ this.taskNumber = taskNumber
+ }
+
+ // gets the task number, the sequencial number of this task from all the subtask ran from this main task stack
+ getTaskNumber() {
+ return this.taskNumber
+ }
+
// Add method to update diffStrategy
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
// If not provided, get from current state
@@ -276,6 +288,7 @@ export class Cline {
await this.providerRef.deref()?.updateTaskHistory({
id: this.taskId,
+ number: this.taskNumber,
ts: lastRelevantMessage.ts,
task: taskMessage.text ?? "",
tokensIn: apiMetrics.totalTokensIn,
diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts
index 3da0c8cdd3d..0afce81bce5 100644
--- a/src/core/__tests__/Cline.test.ts
+++ b/src/core/__tests__/Cline.test.ts
@@ -222,6 +222,7 @@ describe("Cline", () => {
return [
{
id: "123",
+ number: 0,
ts: Date.now(),
task: "historical task",
tokensIn: 100,
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index a8f10f6fcd0..6e9e2025264 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -153,6 +153,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
configManager: ConfigManager
customModesManager: CustomModesManager
+ private lastTaskNumber = -1
constructor(
readonly context: vscode.ExtensionContext,
@@ -180,6 +181,17 @@ 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.
addClineToStack(cline: Cline): void {
+ // 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)
+ }
+ // 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)
}
@@ -192,6 +204,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// make sure no reference kept, once promises end it will be garbage collected
clineToBeRemoved = undefined
}
+ // if the stack is empty, reset the last task number
+ if (this.clineStack.length === 0) {
+ this.lastTaskNumber = -1
+ }
}
// remove the cline object with the received clineId, and all the cline objects bove it in the stack
@@ -520,6 +536,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
historyItem,
experiments,
)
+ // get this cline task number id from the history item and set it to newCline
+ newCline.setTaskNumber(historyItem.number)
this.addClineToStack(newCline)
}
@@ -2384,38 +2402,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await downloadTask(historyItem.ts, apiConversationHistory)
}
+ // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
async deleteTaskWithId(id: string) {
+ // get the task directory full path
+ const { taskDirPath } = await this.getTaskWithId(id)
+
+ // remove task from stack if it's the current task
if (id === this.getCurrentCline()?.taskId) {
await this.removeClineWithIdFromStack(id)
}
- const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
-
+ // delete task from the task history state
await this.deleteTaskFromState(id)
- // Delete the task files.
- const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
-
- if (apiConversationHistoryFileExists) {
- await fs.unlink(apiConversationHistoryFilePath)
- }
-
- const uiMessagesFileExists = await fileExistsAtPath(uiMessagesFilePath)
-
- if (uiMessagesFileExists) {
- await fs.unlink(uiMessagesFilePath)
- }
-
- const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
-
- if (await fileExistsAtPath(legacyMessagesFilePath)) {
- await fs.unlink(legacyMessagesFilePath)
- }
-
+ // check if checkpoints are enabled
const { checkpointsEnabled } = await this.getState()
+ // get the base directory of the project
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
- // Delete checkpoints branch.
+ // delete checkpoints branch from project git repo
if (checkpointsEnabled && baseDir) {
const branchSummary = await simpleGit(baseDir)
.branch(["-D", `roo-code-checkpoints-${id}`])
@@ -2426,22 +2431,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
- // Delete checkpoints directory
- const checkpointsDir = path.join(taskDirPath, "checkpoints")
-
- if (await fileExistsAtPath(checkpointsDir)) {
- try {
- await fs.rm(checkpointsDir, { recursive: true, force: true })
- console.log(`[deleteTaskWithId${id}] removed checkpoints repo`)
- } catch (error) {
- console.error(
- `[deleteTaskWithId${id}] failed to remove checkpoints repo: ${error instanceof Error ? error.message : String(error)}`,
- )
- }
+ // delete the entire task directory including checkpoints and all content
+ try {
+ await fs.rm(taskDirPath, { recursive: true, force: true })
+ console.log(`[deleteTaskWithId${id}] removed task directory`)
+ } catch (error) {
+ console.error(
+ `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
+ )
}
-
- // Succeeds if the dir is empty.
- await fs.rmdir(taskDirPath)
}
async deleteTaskFromState(id: string) {
diff --git a/src/shared/HistoryItem.ts b/src/shared/HistoryItem.ts
index ef242cb9679..e6e2c09ed2b 100644
--- a/src/shared/HistoryItem.ts
+++ b/src/shared/HistoryItem.ts
@@ -1,5 +1,6 @@
export type HistoryItem = {
id: string
+ number: number
ts: number
task: string
tokensIn: number
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index b35be0cd2a6..90d050cf538 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -158,7 +158,10 @@ const TaskHeader: React.FC = ({
flexGrow: 1,
minWidth: 0, // This allows the div to shrink below its content size
}}>
- Task{!isTaskExpanded && ":"}
+
+ Task ({currentTaskItem?.number === 0 ? "Main" : currentTaskItem.number})
+ {!isTaskExpanded && ":"}
+
{!isTaskExpanded && (
{highlightMentions(task.text, false)}
)}
diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx
index b2898fc6a8d..f0484b1dcc8 100644
--- a/webview-ui/src/components/history/HistoryPreview.tsx
+++ b/webview-ui/src/components/history/HistoryPreview.tsx
@@ -120,6 +120,12 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
}}>
{formatDate(item.ts)}
+
+ ({item.number === 0 ? "Main" : item.number})
+
({
const mockTaskHistory = [
{
id: "1",
+ number: 0,
task: "Test task 1",
ts: new Date("2022-02-16T00:00:00").getTime(),
tokensIn: 100,
@@ -31,6 +32,7 @@ const mockTaskHistory = [
},
{
id: "2",
+ number: 0,
task: "Test task 2",
ts: new Date("2022-02-17T00:00:00").getTime(),
tokensIn: 200,
From 8dc8ec32d3d846393d44316110fde3afecc135c8 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Tue, 25 Feb 2025 09:01:50 +0200
Subject: [PATCH 009/541] Fix dependencies and update TaskHeader
---
webview-ui/src/components/chat/TaskHeader.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index 90d050cf538..1ff54310856 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -159,7 +159,7 @@ const TaskHeader: React.FC = ({
minWidth: 0, // This allows the div to shrink below its content size
}}>
- Task ({currentTaskItem?.number === 0 ? "Main" : currentTaskItem.number})
+ Task ({currentTaskItem?.number === 0 ? "Main" : currentTaskItem?.number})
{!isTaskExpanded && ":"}
{!isTaskExpanded && (
From 4744f7f2310353e68a8480f0184aefe6ce9f434e Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Tue, 25 Feb 2025 09:20:43 +0200
Subject: [PATCH 010/541] fixed ClineProvider.test.ts to include
cline.getTaskNumber and cline.setTaskNumber mock
---
src/core/webview/__tests__/ClineProvider.test.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index d7196920e13..50405fb21b2 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -842,6 +842,8 @@ describe("ClineProvider", () => {
apiConversationHistory: mockApiHistory,
overwriteClineMessages: jest.fn(),
overwriteApiConversationHistory: jest.fn(),
+ getTaskNumber: jest.fn(),
+ setTaskNumber: jest.fn(),
taskId: "test-task-id",
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
@@ -894,6 +896,8 @@ describe("ClineProvider", () => {
apiConversationHistory: mockApiHistory,
overwriteClineMessages: jest.fn(),
overwriteApiConversationHistory: jest.fn(),
+ getTaskNumber: jest.fn(),
+ setTaskNumber: jest.fn(),
taskId: "test-task-id",
abortTask: jest.fn(),
handleWebviewAskResponse: jest.fn(),
@@ -925,6 +929,8 @@ describe("ClineProvider", () => {
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
provider.addClineToStack(mockCline)
@@ -1430,6 +1436,8 @@ describe("ClineProvider", () => {
// Setup mock Cline instance
const mockCline = {
api: undefined,
+ getTaskNumber: jest.fn(),
+ setTaskNumber: jest.fn(),
abortTask: jest.fn(),
} as unknown as Cline
provider.addClineToStack(mockCline)
From 1ff403d5ea2e25969ea86a73d305db050200ded8 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Tue, 25 Feb 2025 09:31:04 +0200
Subject: [PATCH 011/541] fixed 2 more places in ClineProvider.test that needed
cline mocks (setTaskNumber, getTaskNumber)
---
.../webview/__tests__/ClineProvider.test.ts | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 50405fb21b2..e8b49cc7fc2 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -409,7 +409,11 @@ describe("ClineProvider", () => {
test("clearTask aborts current task", async () => {
// prepare the mock object
const mockAbortTask = jest.fn()
- const clineMock = { abortTask: mockAbortTask } as unknown as Cline
+ const clineMock = {
+ abortTask: mockAbortTask,
+ getTaskNumber: jest.fn(),
+ setTaskNumber: jest.fn(),
+ } as unknown as Cline
// add the mock object to the stack
provider.addClineToStack(clineMock)
@@ -432,8 +436,16 @@ describe("ClineProvider", () => {
test("addClineToStack adds multiple Cline instances to the stack", () => {
// prepare test data
- const mockCline1 = { taskId: "test-task-id-1" } as unknown as Cline
- const mockCline2 = { taskId: "test-task-id-2" } as unknown as Cline
+ 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
// add Cline instances to the stack
provider.addClineToStack(mockCline1)
From 9b267e9afc8cc4308e5decfa0a9c312caaafc4bf Mon Sep 17 00:00:00 2001
From: Aitor Oses
Date: Tue, 25 Feb 2025 15:30:10 +0100
Subject: [PATCH 012/541] Add Vertex AI prompt caching support and enhance
streaming handling
- Implemented comprehensive prompt caching strategy for Vertex AI models
- Added support for caching system prompts and user message text blocks
- Enhanced stream processing to handle cache-related usage metrics
- Updated model configurations to enable prompt caching
- Improved type definitions for Vertex AI message handling
---
src/api/providers/__tests__/vertex.test.ts | 215 ++++++++++++++++++-
src/api/providers/vertex.ts | 235 ++++++++++++++++++---
src/shared/api.ts | 20 +-
3 files changed, 435 insertions(+), 35 deletions(-)
diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts
index ebe60ba0c68..6e81fd771b7 100644
--- a/src/api/providers/__tests__/vertex.test.ts
+++ b/src/api/providers/__tests__/vertex.test.ts
@@ -4,6 +4,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
import { VertexHandler } from "../vertex"
+import { ApiStreamChunk } from "../../transform/stream"
// Mock Vertex SDK
jest.mock("@anthropic-ai/vertex-sdk", () => ({
@@ -128,7 +129,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -158,8 +159,29 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
- system: systemPrompt,
- messages: mockMessages,
+ system: [
+ {
+ type: "text",
+ text: "You are a helpful assistant",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ messages: [
+ {
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Hello",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: "Hi there!",
+ },
+ ],
stream: true,
})
})
@@ -196,7 +218,7 @@ describe("VertexHandler", () => {
;(handler["client"].messages as any).create = mockCreate
const stream = handler.createMessage(systemPrompt, mockMessages)
- const chunks = []
+ const chunks: ApiStreamChunk[] = []
for await (const chunk of stream) {
chunks.push(chunk)
@@ -230,6 +252,183 @@ describe("VertexHandler", () => {
}
}).rejects.toThrow("Vertex API error")
})
+
+ it("should handle prompt caching for supported models", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ cache_creation_input_tokens: 3,
+ cache_read_input_tokens: 2,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "text",
+ text: "Hello",
+ },
+ },
+ {
+ type: "content_block_delta",
+ delta: {
+ type: "text_delta",
+ text: " world!",
+ },
+ },
+ {
+ type: "message_delta",
+ usage: {
+ output_tokens: 5,
+ },
+ },
+ ]
+
+ 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, [
+ {
+ role: "user",
+ content: "First message",
+ },
+ {
+ role: "assistant",
+ content: "Response",
+ },
+ {
+ role: "user",
+ content: "Second message",
+ },
+ ])
+
+ const chunks: ApiStreamChunk[] = []
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+
+ // Verify usage information
+ const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+ expect(usageChunks).toHaveLength(2)
+ expect(usageChunks[0]).toEqual({
+ type: "usage",
+ inputTokens: 10,
+ outputTokens: 0,
+ cacheWriteTokens: 3,
+ cacheReadTokens: 2,
+ })
+ expect(usageChunks[1]).toEqual({
+ type: "usage",
+ inputTokens: 0,
+ outputTokens: 5,
+ })
+
+ // Verify text content
+ const textChunks = chunks.filter((chunk) => chunk.type === "text")
+ expect(textChunks).toHaveLength(2)
+ expect(textChunks[0].text).toBe("Hello")
+ expect(textChunks[1].text).toBe(" world!")
+
+ // Verify cache control was added correctly
+ expect(mockCreate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ system: [
+ {
+ type: "text",
+ text: "You are a helpful assistant",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ messages: [
+ expect.objectContaining({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "First message",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ }),
+ expect.objectContaining({
+ role: "assistant",
+ content: "Response",
+ }),
+ expect.objectContaining({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Second message",
+ cache_control: { type: "ephemeral" },
+ },
+ ],
+ }),
+ ],
+ }),
+ )
+ })
+
+ it("should handle cache-related usage metrics", async () => {
+ const mockStream = [
+ {
+ type: "message_start",
+ message: {
+ usage: {
+ input_tokens: 10,
+ output_tokens: 0,
+ cache_creation_input_tokens: 5,
+ cache_read_input_tokens: 3,
+ },
+ },
+ },
+ {
+ type: "content_block_start",
+ index: 0,
+ content_block: {
+ type: "text",
+ text: "Hello",
+ },
+ },
+ ]
+
+ 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)
+ }
+
+ // Check for cache-related metrics in usage chunk
+ const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
+ expect(usageChunks.length).toBeGreaterThan(0)
+ expect(usageChunks[0]).toHaveProperty("cacheWriteTokens", 5)
+ expect(usageChunks[0]).toHaveProperty("cacheReadTokens", 3)
+ })
})
describe("completePrompt", () => {
@@ -240,7 +439,13 @@ describe("VertexHandler", () => {
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
- messages: [{ role: "user", content: "Test prompt" }],
+ system: "",
+ messages: [
+ {
+ role: "user",
+ content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
+ },
+ ],
stream: false,
})
})
diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts
index 0ee22e5893d..70562766c3b 100644
--- a/src/api/providers/vertex.ts
+++ b/src/api/providers/vertex.ts
@@ -1,9 +1,86 @@
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 { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
+// Types for Vertex SDK
+
+/**
+ * Vertex API has specific limitations for prompt caching:
+ * 1. Maximum of 4 blocks can have cache_control
+ * 2. Only text blocks can be cached (images and other content types cannot)
+ * 3. Cache control can only be applied to user messages, not assistant messages
+ *
+ * Our caching strategy:
+ * - Cache the system prompt (1 block)
+ * - Cache the last text block of the second-to-last user message (1 block)
+ * - Cache the last text block of the last user message (1 block)
+ * This ensures we stay under the 4-block limit while maintaining effective caching
+ * for the most relevant context.
+ */
+
+interface VertexTextBlock {
+ type: "text"
+ text: string
+ cache_control?: { type: "ephemeral" }
+}
+
+interface VertexImageBlock {
+ type: "image"
+ source: {
+ type: "base64"
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp"
+ data: string
+ }
+}
+
+type VertexContentBlock = VertexTextBlock | VertexImageBlock
+
+interface VertexUsage {
+ input_tokens?: number
+ output_tokens?: number
+ cache_creation_input_tokens?: number
+ cache_read_input_tokens?: number
+}
+
+interface VertexMessage extends Omit {
+ content: string | VertexContentBlock[]
+}
+
+interface VertexMessageCreateParams {
+ model: string
+ max_tokens: number
+ temperature: number
+ system: string | VertexTextBlock[]
+ messages: VertexMessage[]
+ stream: boolean
+}
+
+interface VertexMessageResponse {
+ content: Array<{ type: "text"; text: string }>
+}
+
+interface VertexMessageStreamEvent {
+ type: "message_start" | "message_delta" | "content_block_start" | "content_block_delta"
+ message?: {
+ usage: VertexUsage
+ }
+ usage?: {
+ output_tokens: number
+ }
+ content_block?: {
+ type: "text"
+ text: string
+ }
+ index?: number
+ delta?: {
+ type: "text_delta"
+ text: string
+ }
+}
+
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
export class VertexHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
@@ -18,37 +95,120 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
})
}
+ private formatMessageForCache(message: Anthropic.Messages.MessageParam, shouldCache: boolean): VertexMessage {
+ // Assistant messages are kept as-is since they can't be cached
+ if (message.role === "assistant") {
+ return message as VertexMessage
+ }
+
+ // For string content, we convert to array format with optional cache control
+ if (typeof message.content === "string") {
+ return {
+ ...message,
+ content: [
+ {
+ type: "text" as const,
+ text: message.content,
+ // For string content, we only have one block so it's always the last
+ ...(shouldCache && { cache_control: { type: "ephemeral" } }),
+ },
+ ],
+ }
+ }
+
+ // For array content, find the last text block index once before mapping
+ const lastTextBlockIndex = message.content.reduce(
+ (lastIndex, content, index) => (content.type === "text" ? index : lastIndex),
+ -1,
+ )
+
+ // Then use this pre-calculated index in the map function
+ return {
+ ...message,
+ content: message.content.map((content, contentIndex) => {
+ // Images and other non-text content are passed through unchanged
+ if (content.type === "image") {
+ return content as VertexImageBlock
+ }
+
+ // Check if this is the last text block using our pre-calculated index
+ const isLastTextBlock = contentIndex === lastTextBlockIndex
+
+ return {
+ type: "text" as const,
+ text: (content as { text: string }).text,
+ ...(shouldCache && isLastTextBlock && { cache_control: { type: "ephemeral" } }),
+ }
+ }),
+ }
+ }
+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
- const stream = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ const useCache = model.info.supportsPromptCache
+
+ // Find indices of user messages that we want to cache
+ // We only cache the last two user messages to stay within the 4-block limit
+ // (1 block for system + 1 block each for last two user messages = 3 total)
+ const userMsgIndices = useCache
+ ? messages.reduce((acc, msg, i) => (msg.role === "user" ? [...acc, i] : acc), [] as number[])
+ : []
+ const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
+ const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
+
+ // Create the stream with appropriate caching configuration
+ const params = {
+ model: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- system: systemPrompt,
- messages,
+ // Cache the system prompt if caching is enabled
+ system: useCache
+ ? [
+ {
+ text: systemPrompt,
+ type: "text" as const,
+ cache_control: { type: "ephemeral" },
+ },
+ ]
+ : systemPrompt,
+ messages: messages.map((message, index) => {
+ // Only cache the last two user messages
+ const shouldCache = useCache && (index === lastUserMsgIndex || index === secondLastMsgUserIndex)
+ return this.formatMessageForCache(message, shouldCache)
+ }),
stream: true,
- })
+ }
+
+ const stream = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsStreaming,
+ )) as unknown as AnthropicStream
+
+ // Process the stream chunks
for await (const chunk of stream) {
switch (chunk.type) {
- case "message_start":
- const usage = chunk.message.usage
+ case "message_start": {
+ const usage = chunk.message!.usage
yield {
type: "usage",
inputTokens: usage.input_tokens || 0,
outputTokens: usage.output_tokens || 0,
+ cacheWriteTokens: usage.cache_creation_input_tokens,
+ cacheReadTokens: usage.cache_read_input_tokens,
}
break
- case "message_delta":
+ }
+ case "message_delta": {
yield {
type: "usage",
inputTokens: 0,
- outputTokens: chunk.usage.output_tokens || 0,
+ outputTokens: chunk.usage!.output_tokens || 0,
}
break
-
- case "content_block_start":
- switch (chunk.content_block.type) {
- case "text":
- if (chunk.index > 0) {
+ }
+ case "content_block_start": {
+ switch (chunk.content_block!.type) {
+ case "text": {
+ if (chunk.index! > 0) {
yield {
type: "text",
text: "\n",
@@ -56,21 +216,25 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
}
yield {
type: "text",
- text: chunk.content_block.text,
+ text: chunk.content_block!.text,
}
break
+ }
}
break
- case "content_block_delta":
- switch (chunk.delta.type) {
- case "text_delta":
+ }
+ case "content_block_delta": {
+ switch (chunk.delta!.type) {
+ case "text_delta": {
yield {
type: "text",
- text: chunk.delta.text,
+ text: chunk.delta!.text,
}
break
+ }
}
break
+ }
}
}
}
@@ -86,13 +250,34 @@ export class VertexHandler implements ApiHandler, SingleCompletionHandler {
async completePrompt(prompt: string): Promise {
try {
- const response = await this.client.messages.create({
- model: this.getModel().id,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ const model = this.getModel()
+ const useCache = model.info.supportsPromptCache
+
+ const params = {
+ model: model.id,
+ max_tokens: model.info.maxTokens || 8192,
temperature: this.options.modelTemperature ?? 0,
- messages: [{ role: "user", content: prompt }],
+ system: "", // No system prompt needed for single completions
+ messages: [
+ {
+ role: "user",
+ content: useCache
+ ? [
+ {
+ type: "text" as const,
+ text: prompt,
+ cache_control: { type: "ephemeral" },
+ },
+ ]
+ : prompt,
+ },
+ ],
stream: false,
- })
+ }
+
+ const response = (await this.client.messages.create(
+ params as Anthropic.Messages.MessageCreateParamsNonStreaming,
+ )) as unknown as VertexMessageResponse
const content = response.content[0]
if (content.type === "text") {
diff --git a/src/shared/api.ts b/src/shared/api.ts
index cea760c7760..95399cca4aa 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -435,41 +435,51 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet@20240620": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
},
"claude-3-5-haiku@20241022": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 5.0,
+ cacheWritesPrice: 1.25,
+ cacheReadsPrice: 0.1,
},
"claude-3-opus@20240229": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 15.0,
outputPrice: 75.0,
+ cacheWritesPrice: 18.75,
+ cacheReadsPrice: 1.5,
},
"claude-3-haiku@20240307": {
maxTokens: 4096,
contextWindow: 200_000,
supportsImages: true,
- supportsPromptCache: false,
+ supportsPromptCache: true,
inputPrice: 0.25,
outputPrice: 1.25,
+ cacheWritesPrice: 0.3,
+ cacheReadsPrice: 0.03,
},
} as const satisfies Record
From 36cfa31d20273880a0e1b84cca85d625d917cce7 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Wed, 26 Feb 2025 12:28:01 +0200
Subject: [PATCH 013/541] 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 e8b49cc7fc2..fe45aaa801c 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 73c7fe777bb995acd384f719d8ef78d3aa238238 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Thu, 27 Feb 2025 00:24:54 +0200
Subject: [PATCH 014/541] 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 fe45aaa801c..90d2da45f40 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 5c5bf8502094fb87397eebadda89acd3512dcf84 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Wed, 26 Feb 2025 21:36:19 -0500
Subject: [PATCH 015/541] 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 00000000000..a24286b6bbe
--- /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 5597350db3c..4e85c10575d 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 016/541] 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 a2e96606efc..93d0c9d7500 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 be2b2a97984..dcbe0851477 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -798,6 +798,7 @@ const ChatTextArea = forwardRef(
{
const value = e.target.value
if (value === "prompts-action") {
@@ -849,6 +850,7 @@ const ChatTextArea = forwardRef(
{
const value = e.target.value
if (value === "settings-action") {
@@ -915,6 +917,7 @@ const ChatTextArea = forwardRef(
role="button"
aria-label="enhance prompt"
data-testid="enhance-prompt-button"
+ title="Enhance prompt with additional context"
className={`input-icon-button ${
textAreaDisabled ? "disabled" : ""
} codicon codicon-sparkle`}
@@ -927,11 +930,13 @@ const ChatTextArea = forwardRef(
className={`input-icon-button ${
shouldDisableImages ? "disabled" : ""
} codicon codicon-device-camera`}
+ title="Add images to message"
onClick={() => !shouldDisableImages && onSelectImages()}
style={{ fontSize: 16.5 }}
/>
!textAreaDisabled && onSend()}
style={{ fontSize: 15 }}
/>
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 98369cf095c..fcd1ba9a3b4 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1077,7 +1077,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
- }}>
+ }}
+ title="Scroll to bottom of chat">
@@ -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 341855f796e..fb7db6f6173 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 }) => (
-
vscode.postMessage({ type: "exportCurrentTask" })}>
+ vscode.postMessage({ type: "exportCurrentTask" })}>
{!!item?.size && item.size > 0 && (
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
{prettyBytes(item.size)}
From 10c6f8fb67bc358a8a57e16a103205f113ec6687 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Thu, 27 Feb 2025 01:31:30 -0500
Subject: [PATCH 017/541] Graduate checkpoints out of beta
---
.changeset/eighty-cheetahs-fetch.md | 5 +++
src/core/Cline.ts | 18 ++++----
src/core/webview/ClineProvider.ts | 30 ++++++-------
.../webview/__tests__/ClineProvider.test.ts | 4 +-
src/shared/ExtensionMessage.ts | 2 +-
src/shared/WebviewMessage.ts | 2 +-
src/shared/globalState.ts | 2 +-
.../src/components/chat/Announcement.tsx | 45 +++++++++----------
.../src/components/settings/SettingsView.tsx | 45 +++++++++----------
.../src/context/ExtensionStateContext.tsx | 6 +--
10 files changed, 80 insertions(+), 79 deletions(-)
create mode 100644 .changeset/eighty-cheetahs-fetch.md
diff --git a/.changeset/eighty-cheetahs-fetch.md b/.changeset/eighty-cheetahs-fetch.md
new file mode 100644
index 00000000000..ca103880c84
--- /dev/null
+++ b/.changeset/eighty-cheetahs-fetch.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Graduate checkpoints out of beta
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 532b9cbe990..00897eecf4a 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -115,7 +115,7 @@ export class Cline {
isInitialized = false
// checkpoints
- checkpointsEnabled: boolean = false
+ enableCheckpoints: boolean = false
private checkpointService?: CheckpointService
// streaming
@@ -159,7 +159,7 @@ export class Cline {
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
this.providerRef = new WeakRef(provider)
this.diffViewProvider = new DiffViewProvider(cwd)
- this.checkpointsEnabled = enableCheckpoints ?? false
+ this.enableCheckpoints = enableCheckpoints ?? false
if (historyItem) {
this.taskId = historyItem.id
@@ -3337,7 +3337,7 @@ export class Cline {
// Checkpoints
private async getCheckpointService() {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
throw new Error("Checkpoints are disabled")
}
@@ -3378,7 +3378,7 @@ export class Cline {
commitHash: string
mode: "full" | "checkpoint"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3417,12 +3417,12 @@ export class Cline {
)
} catch (err) {
this.providerRef.deref()?.log("[checkpointDiff] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
public async checkpointSave({ isFirst }: { isFirst: boolean }) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3443,7 +3443,7 @@ export class Cline {
}
} catch (err) {
this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
@@ -3456,7 +3456,7 @@ export class Cline {
commitHash: string
mode: "preview" | "restore"
}) {
- if (!this.checkpointsEnabled) {
+ if (!this.enableCheckpoints) {
return
}
@@ -3511,7 +3511,7 @@ export class Cline {
this.providerRef.deref()?.cancelTask()
} catch (err) {
this.providerRef.deref()?.log("[checkpointRestore] disabling checkpoints for this task")
- this.checkpointsEnabled = false
+ this.enableCheckpoints = false
}
}
}
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 5e6170e2eee..633c7d7293d 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -64,7 +64,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
protected mcpHub?: McpHub // Change from private to protected
- private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
+ private latestAnnouncementId = "feb-27-2025-automatic-checkpoints" // update to some unique identifier when we add a new announcement
configManager: ConfigManager
customModesManager: CustomModesManager
@@ -317,7 +317,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -332,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
task,
images,
@@ -347,7 +347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
mode,
customInstructions: globalInstructions,
@@ -362,7 +362,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions: effectiveInstructions,
enableDiff: diffEnabled,
- enableCheckpoints: checkpointsEnabled,
+ enableCheckpoints,
fuzzyMatchThreshold,
historyItem,
experiments,
@@ -1017,9 +1017,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("diffEnabled", diffEnabled)
await this.postStateToWebview()
break
- case "checkpointsEnabled":
- const checkpointsEnabled = message.bool ?? false
- await this.updateGlobalState("checkpointsEnabled", checkpointsEnabled)
+ case "enableCheckpoints":
+ const enableCheckpoints = message.bool ?? true
+ await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
await this.postStateToWebview()
break
case "browserViewportSize":
@@ -1939,11 +1939,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await fs.unlink(legacyMessagesFilePath)
}
- const { checkpointsEnabled } = await this.getState()
+ const { enableCheckpoints } = await this.getState()
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Delete checkpoints branch.
- if (checkpointsEnabled && baseDir) {
+ if (enableCheckpoints && baseDir) {
const branchSummary = await simpleGit(baseDir)
.branch(["-D", `roo-code-checkpoints-${id}`])
.catch(() => undefined)
@@ -1999,7 +1999,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowModeSwitch,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
taskHistory,
soundVolume,
browserViewportSize,
@@ -2048,7 +2048,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
soundVolume: soundVolume ?? 0.5,
@@ -2181,7 +2181,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
- checkpointsEnabled,
+ enableCheckpoints,
soundVolume,
browserViewportSize,
fuzzyMatchThreshold,
@@ -2265,7 +2265,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("allowedCommands") as Promise,
this.getGlobalState("soundEnabled") as Promise,
this.getGlobalState("diffEnabled") as Promise,
- this.getGlobalState("checkpointsEnabled") as Promise,
+ this.getGlobalState("enableCheckpoints") as Promise,
this.getGlobalState("soundVolume") as Promise,
this.getGlobalState("browserViewportSize") as Promise,
this.getGlobalState("fuzzyMatchThreshold") as Promise,
@@ -2376,7 +2376,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? true,
- checkpointsEnabled: checkpointsEnabled ?? false,
+ enableCheckpoints: enableCheckpoints ?? true,
soundVolume,
browserViewportSize: browserViewportSize ?? "900x600",
screenshotQuality: screenshotQuality ?? 75,
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index 6449cc93bec..c8742cd3f41 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -369,7 +369,7 @@ describe("ClineProvider", () => {
uriScheme: "vscode",
soundEnabled: false,
diffEnabled: false,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
writeDelayMs: 1000,
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
@@ -677,7 +677,7 @@ describe("ClineProvider", () => {
},
mode: "code",
diffEnabled: true,
- checkpointsEnabled: false,
+ enableCheckpoints: false,
fuzzyMatchThreshold: 1.0,
experiments: experimentDefault,
} as any)
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index e87edffed16..34abd38dbf2 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -111,7 +111,7 @@ export interface ExtensionState {
soundEnabled?: boolean
soundVolume?: number
diffEnabled?: boolean
- checkpointsEnabled: boolean
+ enableCheckpoints: boolean
browserViewportSize?: string
screenshotQuality?: number
fuzzyMatchThreshold?: number
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index fde7442cc1d..8d3a114e65b 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -52,7 +52,7 @@ export interface WebviewMessage {
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "openMcpSettings"
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
index 2cc90456a76..0863b34db22 100644
--- a/src/shared/globalState.ts
+++ b/src/shared/globalState.ts
@@ -53,7 +53,7 @@ export type GlobalStateKey =
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
- | "checkpointsEnabled"
+ | "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index a2e96606efc..13c77fe4424 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -1,8 +1,5 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
-// import VSCodeButtonLink from "./VSCodeButtonLink"
-// import { getOpenRouterAuthUrl } from "./ApiOptions"
-// import { vscode } from "../utils/vscode"
interface AnnouncementProps {
version: string
@@ -28,36 +25,38 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
style={{ position: "absolute", top: "8px", right: "8px" }}>
- 🎉{" "}Introducing Roo Code 3.2
+ 🎉{" "}Automatic Checkpoints Now Enabled
- Our biggest update yet is here - we're officially changing our name from Roo Cline to Roo Code! After
- growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to
- everyone in the Cline community who helped us reach this milestone.
+ We're thrilled to announce that our experimental Checkpoints feature is now enabled by default for all
+ users. This powerful feature automatically tracks your project changes during a task, allowing you to
+ quickly review or revert to earlier states if needed.
- Custom Modes: Celebrating Our New Identity
+ What's New
- To mark this new chapter, we're introducing the power to shape Roo Code into any role you need! Create
- specialized personas and create an entire team of agents with deeply customized prompts:
+ Automatic Checkpoints provide you with:
- QA Engineers who write thorough test cases and catch edge cases
- Product Managers who excel at user stories and feature prioritization
- UI/UX Designers who craft beautiful, accessible interfaces
- Code Reviewers who ensure quality and maintainability
+ Peace of mind when making significant changes
+ Ability to visually inspect changes between steps
+ Easy rollback if you're not satisfied with certain code modifications
+ Improved navigation through complex task execution
- Just click the icon to
- get started with Custom Modes!
- Join Us for the Next Chapter
+ Customize Your Experience
- We can't wait to see how you'll push Roo Code's potential even further! Share your custom modes and join
- the discussion at{" "}
-
- reddit.com/r/RooCode
-
- .
+ While we recommend keeping this feature enabled, you can disable it if needed.{" "}
+ {
+ e.preventDefault()
+ window.postMessage({ type: "action", action: "settingsButtonClicked" }, "*")
+ }}
+ style={{ display: "inline", padding: "0 2px" }}>
+ Open Settings
+ {" "}
+ and look for the "Enable automatic checkpoints" option in the Advanced Settings section.
)
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index d3e65a99ea8..51ef4fe81d6 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 ae5c5b95398..3dfc87de750 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 018/541] 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 5cda3330318..cd6aead1a59 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 019/541] 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 211d06aa199..bdae7b5b26a 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 69e257e7a51..8cc895f291e 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 f68c9acd1fb..85572632ec2 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 00000000000..8454a7c9af8
--- /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 5e6170e2eee..1c2ffea5508 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 a05afa46512..3b148a41ac9 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 e87edffed16..16a18043f57 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 fde7442cc1d..2b0c68f7bea 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 e7e4c54db6a..68b2f87c45f 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 3ae441cd52f..5909a3eaef2 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 00000000000..ea306d11d70
--- /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 (
+ !open && handleCancel()}>
+
+
+ Human Relay - Please Help Copy and Paste Information
+
+ Please copy the text below to the web AI, then paste the AI's response into the input box below.
+
+
+
+
+
+
+
+ {isCopyClicked ? : }
+
+
+
+ {isCopyClicked &&
Copied to clipboard
}
+
+
+
Please enter the AI's response:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Submit
+
+
+
+
+ )
+}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c30035cef01..c8598ec1014 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 020/541] Fix AnthropicHandler#completePrompt
---
src/api/providers/anthropic.ts | 94 +++++++++++++++++-----------------
1 file changed, 47 insertions(+), 47 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 8c5a1795b1f..eca81eab2e2 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]
- return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
- }
+ // 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"
+ }
- 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,
- })
+ const maxTokens = this.options.modelMaxTokens || info.maxTokens || 8192
- const content = response.content[0]
+ if (info.thinking) {
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ temperature = 1.0
- if (content.type === "text") {
- return content.text
- }
+ // 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,
+ )
- return ""
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(`Anthropic completion error: ${error.message}`)
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
}
- throw error
+ return { id, info, temperature, maxTokens, thinking }
}
+
+ 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) {
+ let { id: modelId, temperature, maxTokens, thinking } = this.getModel()
+
+ const message = await this.client.messages.create({
+ model: modelId,
+ max_tokens: maxTokens,
+ temperature,
+ thinking,
+ messages: [{ role: "user", content: prompt }],
+ stream: false,
+ })
+
+ 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 021/541] 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 ff7bdb40549..82e098f65fb 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 022/541] 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 00000000000..fe2183052d3
--- /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 023/541] 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 a24286b6bbe..00000000000
--- 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 4e85c10575d..5597350db3c 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 024/541] 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 ca103880c84..00000000000
--- 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 fe2183052d3..00000000000
--- 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 13b06953354..ff5d8d6aaf2 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 808e2f2f107..c1f748983fc 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 463e9d597a2..8441488bac3 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 025/541] Update CHANGELOG.md
---
CHANGELOG.md | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff5d8d6aaf2..d0cf8f79c3a 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 026/541] 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 e7e4c54db6a..d2b4ed728fb 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 027/541] 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 d5f076247a6..e496a7fa510 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 6bd00e95673..836279bfe45 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 00000000000..7594c13e6d9
--- /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 00000000000..eca2b98b8d8
--- /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 91bbd073870..90791f63586 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 061fa789de4..2bfafeff5c6 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 028/541] 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 00000000000..f0128f21e0d
--- /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 d6a855849c5..eda744c335c 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 442282d5876..47b023ce6fd 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 029/541] 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 f0128f21e0d..00000000000
--- 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 d0cf8f79c3a..e3aa95d448e 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 c1f748983fc..e7d7718b754 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 8441488bac3..a4a2298a48f 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 030/541] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3aa95d448e..fe8156abf79 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 031/541] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe8156abf79..9622ce0c99e 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 032/541] 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 397d3aca805..f1b4d2f4d40 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 4fa9595ba68..7f412eefde6 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 90d2da45f40..bb646ccef83 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 033/541] 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 c1f748983fc..547f20a9305 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 8441488bac3..35db01621ab 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 70562766c3b..69fb7d26f78 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 633c7d7293d..5417e54ff73 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 442282d5876..f048761d0f3 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 0863b34db22..05b868a450c 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 c30035cef01..42ac5cdcb30 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 034/541] 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 6e81fd771b7..076f902ca2b 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 73394bae104..65ae1370035 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 00000000000..54f6b1037b4
--- /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 035/541] 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 05b868a450c..6e29e038357 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 036/541] 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 6e29e038357..05b868a450c 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 037/541] 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 05b868a450c..6e29e038357 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 038/541] 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 076f902ca2b..9cf92f0a16b 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 eca81eab2e2..fc0b99c59b1 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 69bcb0074c1..82c02e20a7d 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 69fb7d26f78..a25fad07ee8 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 5417e54ff73..7b6f2c89719 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 62517d69584..c99ddddbc45 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 b36781d630a..f88bb5e8b51 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 6e29e038357..aabc77cc01b 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 d21e1fb7ead..557a69538d5 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 65ae1370035..06ed95585ac 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 54f6b1037b4..212316ea9ac 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 039/541] added logs, error handling, and added console.log to
the provider log method
---
src/core/Cline.ts | 34 +++++---
src/core/webview/ClineProvider.ts | 133 +++++++++++++++++++++---------
2 files changed, 117 insertions(+), 50 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 17c7fd12e2e..fe48d8dbe85 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."}`)
-
- await this.addToApiConversationHistory({
- role: "user",
- content: [
- {
- type: "text",
- text: `[new_task completed] Result: ${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."}`,
+ },
+ ],
+ })
+ } 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 19e385d2464..049b4b03f72 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)
- }
- // if cline.getTaskNumber() > lastTaskNumber, set lastTaskNumber to cline.getTaskNumber()
- else if (cline.getTaskNumber() > this.lastTaskNumber) {
- this.lastTaskNumber = cline.getTaskNumber()
+ 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
}
- // 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 040/541] 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 049b4b03f72..5bbd9c2fec6 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 041/541] 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 8454a7c9af8..85292a27dca 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 1c2ffea5508..e781a36dbe6 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 3b148a41ac9..87dc7405a35 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 2b0c68f7bea..a45a7272531 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 c8598ec1014..b125014a629 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 042/541] 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 85292a27dca..2911e24eaff 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 043/541] 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 fe48d8dbe85..d05f9320d4a 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 5bbd9c2fec6..4249081277d 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 987baeefaf6..7700ae890ed 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 044/541] fix: Optimize panel management, support panel
references for sidebar and tab types
---
src/activate/registerCommands.ts | 39 +++++++++-----
src/api/providers/human-relay.ts | 51 ++-----------------
src/core/webview/ClineProvider.ts | 10 ++++
.../human-relay/HumanRelayDialog.tsx | 12 ++++-
4 files changed, 52 insertions(+), 60 deletions(-)
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 8cc895f291e..b520e0cb8e4 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
-
-// Get the panel function for command access
-export function getPanel(): vscode.WebviewPanel | undefined {
- return panel
+// Store panel references in both modes
+let sidebarPanel: vscode.WebviewView | undefined = undefined
+let tabPanel: vscode.WebviewPanel | undefined = undefined
+
+/**
+ * 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 2911e24eaff..618ae847de6 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 e781a36dbe6..c87406d3d46 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 ea306d11d70..61d4cbe2132 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 045/541] 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 d05f9320d4a..9cb0228e3e3 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 4249081277d..f92650f887d 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 046/541] 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 5909a3eaef2..8c37236adbe 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 047/541] Delete task confirmation enhancements
---
.changeset/chilly-bugs-pay.md | 5 ++
webview-ui/src/components/chat/TaskHeader.tsx | 84 ++++++++++++++-----
.../components/history/DeleteTaskDialog.tsx | 42 ++++++----
.../src/components/history/HistoryView.tsx | 30 +++----
.../history/__tests__/HistoryView.test.tsx | 60 +++++++++----
5 files changed, 148 insertions(+), 73 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 00000000000..b30f8241ef7
--- /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 fb7db6f6173..319a9aeccd3 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -3,15 +3,18 @@ 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 { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
+import { HistoryItem } from "../../../../src/shared/HistoryItem"
+
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 { DeleteTaskDialog } from "../history/DeleteTaskDialog"
interface TaskHeaderProps {
task: ClineMessage
@@ -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 b40adeae3de..31d85abd370 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 ca60e1fcb89..49d71e5ddd9 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 12b0181af6b..4b761d6fc4b 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)
+
+ // Click delete button to open confirmation dialog
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton)
+
+ // Verify dialog is shown
+ const dialog = screen.getByRole("alertdialog")
+ expect(dialog).toBeInTheDocument()
+
+ // 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",
+ })
+ })
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
+ it("deletes immediately on shift-click without confirmation", () => {
+ const onDone = jest.fn()
+ render( )
- // Click delete button to open confirmation dialog
- const deleteButton = within(taskContainer).getByTitle("Delete Task")
- fireEvent.click(deleteButton)
+ // Find and hover over first task
+ const taskContainer = screen.getByTestId("virtuoso-item-1")
+ fireEvent.mouseEnter(taskContainer)
- // Find and click the confirm delete button in the dialog
- const confirmDeleteButton = screen.getByRole("button", { name: /delete/i })
- fireEvent.click(confirmDeleteButton)
+ // Shift-click delete button
+ const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
+ fireEvent.click(deleteButton, { shiftKey: true })
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "deleteTaskWithId",
- text: "1",
+ // 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 048/541] chore: Restore .gitignore, remove unnecessary file
rules
---
.gitignore | 5 -----
1 file changed, 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index bdae7b5b26a..211d06aa199 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 049/541] 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 4017ccf318e..1533bba3a8f 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 0c9971f2690..fa128990924 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 53025be01a6..74c8463b372 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 050/541] 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 00000000000..1b9c3d94e80
--- /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 051/541] 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 00000000000..0e693b44703
--- /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 00000000000..6617e475bdd
--- /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 b2898fc6a8d..bf53845da7e 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 49d71e5ddd9..d50a569c8d9 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 53025be01a6..0e80d1b0c62 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 00000000000..7377874fd01
--- /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 2e473c9b8ac..12e99962051 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 052/541] 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 00000000000..7382b38c77f
--- /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 82c02e20a7d..70c40c2c279 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 f88bb5e8b51..99e2986e882 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 42ac5cdcb30..6f0dba9f00e 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 06ed95585ac..0b1fb284987 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 212316ea9ac..1e14e945249 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,25 +59,7 @@ describe("ThinkingBudget", () => {
expect(screen.getAllByTestId("slider")).toHaveLength(2)
})
- it("should use modelMaxThinkingTokens field for Anthropic provider", () => {
- const setApiConfigurationField = jest.fn()
-
- render(
- ,
- )
-
- 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", () => {
+ it("should update modelMaxThinkingTokens", () => {
const setApiConfigurationField = jest.fn()
render(
@@ -86,7 +67,6 @@ describe("ThinkingBudget", () => {
{...defaultProps}
apiConfiguration={{ modelMaxThinkingTokens: 4096 }}
setApiConfigurationField={setApiConfigurationField}
- provider="vertex"
/>,
)
From 0a81853d84e43ca92718166db6d07ea984889a39 Mon Sep 17 00:00:00 2001
From: yansheng3
Date: Fri, 28 Feb 2025 19:06:43 +0800
Subject: [PATCH 053/541] 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 00897eecf4a..3127d3d38bf 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 054/541] 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 2bb7a8ee68f..20bd5222f6d 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 055/541] 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 7ee8bb98ad5..85b5c9063c5 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 056/541] 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 85b5c9063c5..de7e461cb9c 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 057/541] Improve terminal command execution output parsing
---
src/core/Cline.ts | 28 +-
src/integrations/terminal/TerminalManager.ts | 193 +++++++-
src/integrations/terminal/TerminalProcess.ts | 421 ++++++++++++------
src/integrations/terminal/TerminalRegistry.ts | 30 ++
.../__tests__/TerminalProcess.test.ts | 157 +++----
.../__tests__/TerminalRegistry.test.ts | 5 +-
src/shared/combineCommandSequences.ts | 2 +-
7 files changed, 585 insertions(+), 251 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 00897eecf4a..fab181b86e4 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 d5496e20fb9..a55f7867d40 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 (startDisposable) {
+ this.disposables.push(startDisposable)
}
- if (disposable) {
- this.disposables.push(disposable)
+ 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 5597350db3c..99ef215e784 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
- 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)
- }
- // 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]*/, "")
+ // 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)
}
- if (lines.length > 1) {
- lines[1] = lines[1].replace(/^[^a-zA-Z0-9]*/, "")
+ })
+ })
+
+ // 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)
}
- // Join lines back
- data = lines.join("\n")
- isFirstChunk = false
- } else {
- data = stripAnsi(data)
- }
+ })
+ })
+
+ // 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
- // 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
- }
+ 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) {
+ // 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
}
- 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, "")
+ // 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
- // 2. Set isHot depending on the command
- // Set to hot to stall API requests until terminal is cool again
+ // 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
+ }
+
+ // 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
- }
+ // Set streamClosed immediately after stream ends
+ if (this.terminalInfo) {
+ this.terminalInfo.streamClosed = true
+ }
- this.fullOutput += data
- if (this.isListening) {
- this.emitIfEol(data)
- this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
- }
+ // 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
}
- this.emitRemainingBufferIfListening()
+ // 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 2fb49e48257..69a21d94fde 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 9ccbaef920e..11c0339f27e 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"
+// npx jest src/integrations/terminal/__tests__/TerminalProcess.test.ts
+
import * as vscode from "vscode"
-import { EventEmitter } from "events"
-// Mock vscode
-jest.mock("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")
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
- // 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)
- })
-
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 cc667a851b9..a2b8fcd3b08 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 31fe219f041..cbd674fc070 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 058/541] 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 11c0339f27e..44cae92580f 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 059/541] 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 fcd1ba9a3b4..786f1ea957c 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 060/541] 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 00000000000..95bb8d3db4e
--- /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 950769b39b4..17cad607a1b 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 fe64af16d65..8e8bb0712a9 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 9cf92f0a16b..d3f34fcfa16 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", () => ({
})),
}))
-describe("VertexHandler", () => {
- let handler: VertexHandler
+// 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,
+ },
+ },
+ }
+ })
- beforeEach(() => {
- handler = new VertexHandler({
- apiModelId: "claude-3-5-sonnet-v2@20241022",
- vertexProjectId: "test-project",
- vertexRegion: "us-central1",
- })
+ 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
+
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 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", async () => {
+ 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 a25fad07ee8..5b8fc1ad789 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 00000000000..bcb26df0992
--- /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 00000000000..75abb7d3bed
--- /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 99e2986e882..b16e5142a0e 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 061/541] Move integration tests into its own module
---
.github/workflows/changeset-release.yml | 2 +-
.github/workflows/code-qa.yml | 16 +-
.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 | 930 +------
package.json | 33 +-
src/exports/cline.d.ts | 93 +
src/shared/modes.ts | 6 +-
19 files changed, 2551 insertions(+), 962 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 a2bcd3f0393..462516365b1 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 fde891f8041..b7292dd9ee4 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: Install dependencies
+ 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: Install dependencies
- run: npm run install:all
- 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 638ac22db76..1fc5a728b04 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 dd7760789b3..16ea4271bae 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 f5882fea1ea..25f54492de0 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 00000000000..278df120c28
--- /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 00000000000..7e0daac064d
--- /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 2576c6072f1..28545efd46b 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 cc487b0bf78..a9540d96004 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 0de0ea736a9..99581107673 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 b0e0839da77..a9f0b93e0d2 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 950769b39b4..a8fa18b5f1b 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",
@@ -10516,25 +9986,13 @@
"integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
"dev": true,
"dependencies": {
- "which-typed-array": "^1.1.14"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "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,
+ "which-typed-array": "^1.1.14"
+ },
"engines": {
- "node": ">=10"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakmap": {
@@ -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 fe64af16d65..b9b62bd5e39 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 fcf93fc10d0..e529947b6b4 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 ceb1e3e11fe..2eda966519c 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 062/541] 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 99581107673..792acb14a0d 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 a8fa18b5f1b..0b24ce6664c 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 b9b62bd5e39..6f5699a7c7f 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 cb897aa8cb0..74b734d7381 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 8b646f933b9..0fb26ac38f2 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 063/541] 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 16ea4271bae..ccc8b495ea9 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 7e0daac064d..630a13f0e7e 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 28545efd46b..2576c6072f1 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 064/541] 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 74b734d7381..dbc0c678c2e 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 0fb26ac38f2..d12e7f337ec 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 065/541] 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 158b96ff2ce..32a520ca2f5 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 f92650f887d..3cd01d275ac 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 066/541] 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 dcbe0851477..a2af6a3a71e 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",
}}>
+
+ {modeShortcutText}
+
{getAllModes(customModes).map((mode) => (
{mode.name}
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index fcd1ba9a3b4..b65857705f0 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -28,6 +28,7 @@ import TaskHeader from "./TaskHeader"
import AutoApproveMenu from "./AutoApproveMenu"
import { AudioType } from "../../../../src/shared/WebviewMessage"
import { validateCommand } from "../../utils/command-validation"
+import { modes } from "../../../../src/shared/modes"
interface ChatViewProps {
isHidden: boolean
@@ -38,6 +39,9 @@ interface ChatViewProps {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
+const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
+const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . for next mode`
+
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const {
version,
@@ -963,6 +967,33 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isWriteToolAction,
])
+ // Add this new function to handle mode switching
+ const switchToNextMode = useCallback(() => {
+ const currentModeIndex = modes.findIndex((m) => m.slug === mode)
+ const nextModeIndex = (currentModeIndex + 1) % modes.length
+ setMode(modes[nextModeIndex].slug)
+ }, [mode, setMode])
+
+ // Add keyboard event handler
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ // Check for Command + . (period)
+ if ((event.metaKey || event.ctrlKey) && event.key === ".") {
+ event.preventDefault() // Prevent default browser behavior
+ switchToNextMode()
+ }
+ },
+ [switchToNextMode],
+ )
+
+ // Add event listener
+ useEffect(() => {
+ window.addEventListener("keydown", handleKeyDown)
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown)
+ }
+ }, [handleKeyDown])
+
return (
diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
index 3241010e886..e7abb1f65e9 100644
--- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
+++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
@@ -45,6 +45,7 @@ describe("ChatTextArea", () => {
onHeightChange: jest.fn(),
mode: defaultModeSlug,
setMode: jest.fn(),
+ modeShortcutText: "(⌘. for next mode)",
}
beforeEach(() => {
From a0684454a22e708bc3c0d79cd2293f10fb2c98b9 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 10:10:24 -0500
Subject: [PATCH 067/541] Add a dynamic token buffer
---
.changeset/swift-lamps-decide.md | 5 ++++
.../__tests__/sliding-window.test.ts | 27 +++++++++++++------
src/core/sliding-window/index.ts | 11 +++++---
3 files changed, 31 insertions(+), 12 deletions(-)
create mode 100644 .changeset/swift-lamps-decide.md
diff --git a/.changeset/swift-lamps-decide.md b/.changeset/swift-lamps-decide.md
new file mode 100644
index 00000000000..b2e7ed3e60c
--- /dev/null
+++ b/.changeset/swift-lamps-decide.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add a dynamic token buffer
diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts
index dbc0c678c2e..df698d955aa 100644
--- a/src/core/sliding-window/__tests__/sliding-window.test.ts
+++ b/src/core/sliding-window/__tests__/sliding-window.test.ts
@@ -3,7 +3,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "../../../shared/api"
-import { TOKEN_BUFFER, estimateTokenCount, truncateConversation, truncateConversationIfNeeded } from "../index"
+import {
+ TOKEN_BUFFER_PERCENTAGE,
+ estimateTokenCount,
+ truncateConversation,
+ truncateConversationIfNeeded,
+} from "../index"
/**
* Tests for the truncateConversation function
@@ -121,10 +126,11 @@ 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: "" }]
+ // Account for the dynamic buffer which is 10% of context window (10,000 tokens)
// Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 44999, // Well below threshold + buffer
+ totalTokens: 39999, // Well below threshold + dynamic buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -148,10 +154,11 @@ 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: "" }]
+ // Account for the dynamic buffer which is 10% of context window (10,000 tokens)
// Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 74999, // Well below threshold + buffer
+ totalTokens: 69999, // Well below threshold + dynamic buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -202,10 +209,11 @@ 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: "" }]
+ // Account for the dynamic buffer which is 10% of context window (20,000 tokens for this test)
// Below max tokens and buffer - no truncation
const result1 = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
- totalTokens: 164999, // Well below threshold + buffer
+ totalTokens: 149999, // Well below threshold + dynamic buffer
contextWindow: modelInfo.contextWindow,
maxTokens: modelInfo.maxTokens,
})
@@ -244,7 +252,8 @@ 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 = 64999 // Well below threshold + buffer
+ const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE // 10000
+ const totalTokens = 70000 - dynamicBuffer - 1 // Just 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: "" }]
@@ -338,7 +347,8 @@ describe("truncateConversationIfNeeded", () => {
]
// Set base tokens so total is well below threshold + buffer even with small content added
- const baseTokensForSmall = availableTokens - smallContentTokens - TOKEN_BUFFER - 10
+ const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE
+ const baseTokensForSmall = availableTokens - smallContentTokens - dynamicBuffer - 10
const resultWithSmall = truncateConversationIfNeeded({
messages: messagesWithSmallContent,
totalTokens: baseTokensForSmall,
@@ -389,10 +399,11 @@ describe("truncateConversationIfNeeded", () => {
expect(resultWithVeryLarge).not.toEqual(messagesWithVeryLargeContent) // Should truncate
})
- it("should truncate if tokens are within TOKEN_BUFFER of the threshold", () => {
+ it("should truncate if tokens are within TOKEN_BUFFER_PERCENTAGE of the threshold", () => {
const modelInfo = createModelInfo(100000, true, 30000)
const maxTokens = 100000 - 30000 // 70000
- const totalTokens = 66000 // Within 5000 of threshold (70000)
+ const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE // 10% of 100000 = 10000
+ const totalTokens = 70000 - dynamicBuffer + 1 // Just within the dynamic buffer 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: "" }]
diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts
index d12e7f337ec..48548ecfc77 100644
--- a/src/core/sliding-window/index.ts
+++ b/src/core/sliding-window/index.ts
@@ -4,7 +4,10 @@ import { Tiktoken } from "js-tiktoken/lite"
import o200kBase from "js-tiktoken/ranks/o200k_base"
export const TOKEN_FUDGE_FACTOR = 1.5
-export const TOKEN_BUFFER = 5000
+/**
+ * Default percentage of the context window to use as a buffer when deciding when to truncate
+ */
+export const TOKEN_BUFFER_PERCENTAGE = 0.1
/**
* Counts tokens for user content using tiktoken for text
@@ -108,9 +111,9 @@ export function truncateConversationIfNeeded({
const effectiveTokens = totalTokens + lastMessageTokens
// Calculate available tokens for conversation history
- const allowedTokens = contextWindow - reservedTokens
+ // Truncate if we're within TOKEN_BUFFER_PERCENTAGE of the context window
+ const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens
// Determine if truncation is needed and apply if necessary
- // Truncate if we're within TOKEN_BUFFER of the limit
- return effectiveTokens > allowedTokens - TOKEN_BUFFER ? truncateConversation(messages, 0.5) : messages
+ return effectiveTokens > allowedTokens ? truncateConversation(messages, 0.5) : messages
}
From 163528eee99f96b4f103ecbd2d7d630ab65a7e43 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sat, 1 Mar 2025 15:15:46 +0000
Subject: [PATCH 068/541] changeset version bump
---
.changeset/chilly-bugs-pay.md | 5 -----
.changeset/swift-lamps-decide.md | 5 -----
.changeset/tasty-grapes-suffer.md | 5 -----
.changeset/young-hornets-taste.md | 5 -----
CHANGELOG.md | 9 +++++++++
package-lock.json | 4 ++--
package.json | 2 +-
7 files changed, 12 insertions(+), 23 deletions(-)
delete mode 100644 .changeset/chilly-bugs-pay.md
delete mode 100644 .changeset/swift-lamps-decide.md
delete mode 100644 .changeset/tasty-grapes-suffer.md
delete mode 100644 .changeset/young-hornets-taste.md
diff --git a/.changeset/chilly-bugs-pay.md b/.changeset/chilly-bugs-pay.md
deleted file mode 100644
index b30f8241ef7..00000000000
--- a/.changeset/chilly-bugs-pay.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Delete task confirmation enhancements
diff --git a/.changeset/swift-lamps-decide.md b/.changeset/swift-lamps-decide.md
deleted file mode 100644
index b2e7ed3e60c..00000000000
--- a/.changeset/swift-lamps-decide.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add a dynamic token buffer
diff --git a/.changeset/tasty-grapes-suffer.md b/.changeset/tasty-grapes-suffer.md
deleted file mode 100644
index 7382b38c77f..00000000000
--- a/.changeset/tasty-grapes-suffer.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Fix maxTokens defaults for Claude 3.7 Sonnet models
diff --git a/.changeset/young-hornets-taste.md b/.changeset/young-hornets-taste.md
deleted file mode 100644
index 1b9c3d94e80..00000000000
--- a/.changeset/young-hornets-taste.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Prettier thinking blocks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9622ce0c99e..944fd19e548 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Roo Code Changelog
+## 3.7.9
+
+### Patch Changes
+
+- Delete task confirmation enhancements
+- Add a dynamic token buffer
+- Fix maxTokens defaults for Claude 3.7 Sonnet models
+- Prettier thinking blocks
+
## [3.7.8]
- Add Vertex AI prompt caching support for Claude models (thanks @aitoroses and @lupuletic!)
diff --git a/package-lock.json b/package-lock.json
index 0b24ce6664c..72449b850a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.8",
+ "version": "3.7.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.8",
+ "version": "3.7.9",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 6f5699a7c7f..b8427fd3037 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.8",
+ "version": "3.7.9",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 574348d9639202e7eebb4f5ecb6227ebbd3741ba Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 10:22:13 -0500
Subject: [PATCH 069/541] Update CHANGELOG.md
---
CHANGELOG.md | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 944fd19e548..691dbbe0797 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,14 @@
# Roo Code Changelog
-## 3.7.9
-
-### Patch Changes
+## [3.7.9]
- Delete task confirmation enhancements
-- Add a dynamic token buffer
-- Fix maxTokens defaults for Claude 3.7 Sonnet models
+- Smarter context window management
- Prettier thinking blocks
+- Fix maxTokens defaults for Claude 3.7 Sonnet models
+- Terminal output parsing improvements (thanks @KJ7LNW!)
+- UI fix to dropdown hover colors (thanks @SamirSaji!)
+- Add support for Claude Sonnet 3.7 thinking via Vertex AI (thanks @lupuletic!)
## [3.7.8]
From f4a6f06a28d40c5fcc39346f7657921588dbb619 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Sat, 1 Mar 2025 08:18:43 -0800
Subject: [PATCH 070/541] Fix "install dependencies" section of
marketplace-publish
---
.github/workflows/marketplace-publish.yml | 5 +----
package.json | 2 +-
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index fcc089c1db3..794e598b80a 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -29,10 +29,7 @@ jobs:
- name: Install Dependencies
run: |
npm install -g vsce ovsx
- npm install
- cd webview-ui
- npm install
- cd ..
+ npm run install:ci
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
diff --git a/package.json b/package.json
index b8427fd3037..16b1e1e5d43 100644
--- a/package.json
+++ b/package.json
@@ -334,6 +334,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",
@@ -348,7 +349,6 @@
"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",
From 44185198e67e4e2b7afccbdf6a41cd9eb354b859 Mon Sep 17 00:00:00 2001
From: ShayBC
Date: Sat, 1 Mar 2025 20:00:29 +0200
Subject: [PATCH 071/541] added 500ms delay after modeSwitch to avoid a bug of
running subtask before mode switch is actualy performed, also treated state
of command execution where a call to finishSubtask was missing
---
src/core/Cline.ts | 45 ++++++++++++++++++++++++---------------------
1 file changed, 24 insertions(+), 21 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 32a520ca2f5..0690990cc67 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -2628,10 +2628,7 @@ export class Cline {
}
// Switch the mode using shared handler
- const provider = this.providerRef.deref()
- if (provider) {
- await provider.handleModeSwitch(mode_slug)
- }
+ await this.providerRef.deref()?.handleModeSwitch(mode_slug)
pushToolResult(
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
targetMode.name
@@ -2700,23 +2697,18 @@ export class Cline {
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}`,
- )
- // set the isPaused flag to true so the parent task can wait for the sub-task to finish
- this.isPaused = true
- } else {
- pushToolResult(
- formatResponse.toolError("Failed to create new task: provider not available"),
- )
- }
+ await this.providerRef.deref()?.handleModeSwitch(mode)
+ // wait for mode to actually switch in UI and in State
+ await delay(500) // delay to allow mode change to take effect before next tool is executed
+ this.providerRef
+ .deref()
+ ?.log(`[subtasks] Task: ${this.taskNumber} creating new task in '${mode}' mode`)
+ await this.providerRef.deref()?.initClineWithSubTask(message)
+ pushToolResult(
+ `Successfully created new task in ${targetMode.name} mode with message: ${message}`,
+ )
+ // set the isPaused flag to true so the parent task can wait for the sub-task to finish
+ this.isPaused = true
break
}
} catch (error) {
@@ -2772,6 +2764,15 @@ export class Cline {
undefined,
false,
)
+
+ if (this.isSubTask) {
+ // tell the provider to remove the current subtask and resume the previous task in the stack (it might decide to run the command)
+ await this.providerRef
+ .deref()
+ ?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
+ break
+ }
+
await this.ask(
"command",
removeClosingTag("command", command),
@@ -2973,6 +2974,8 @@ export class Cline {
if (currentMode !== this.pausedModeSlug) {
// the mode has changed, we need to switch back to the paused mode
await this.providerRef.deref()?.handleModeSwitch(this.pausedModeSlug)
+ // wait for mode to actually switch in UI and in State
+ await delay(500) // delay to allow mode change to take effect before next tool is executed
this.providerRef
.deref()
?.log(
From 2a7edc9a4015803a15881c0e5f2350e9070e6298 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 18:03:28 -0500
Subject: [PATCH 072/541] Add support for Mermaid diagrams
---
webview-ui/package-lock.json | 1127 ++++++++++++++++-
webview-ui/package.json | 1 +
.../src/components/common/MarkdownBlock.tsx | 25 +-
.../src/components/common/MermaidBlock.tsx | 227 ++++
webview-ui/src/utils/useDebounceEffect.ts | 42 +
5 files changed, 1415 insertions(+), 7 deletions(-)
create mode 100644 webview-ui/src/components/common/MermaidBlock.tsx
create mode 100644 webview-ui/src/utils/useDebounceEffect.ts
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 22564d01a65..b614ba387e1 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -28,6 +28,7 @@
"fast-deep-equal": "^3.1.3",
"fzf": "^0.5.2",
"lucide-react": "^0.475.0",
+ "mermaid": "^11.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.3",
@@ -100,6 +101,26 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
+ "integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==",
+ "dependencies": {
+ "package-manager-detector": "^0.2.8",
+ "tinyexec": "^0.3.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@antfu/utils": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
+ "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -2196,6 +2217,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
+ "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="
+ },
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
+ "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
+ "dependencies": {
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
+ "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
+ "dependencies": {
+ "@chevrotain/types": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
+ "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
+ "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
+ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="
+ },
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
@@ -2785,6 +2845,37 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
+ },
+ "node_modules/@iconify/utils": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
+ "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.0.0",
+ "@antfu/utils": "^8.1.0",
+ "@iconify/types": "^2.0.0",
+ "debug": "^4.4.0",
+ "globals": "^15.14.0",
+ "kolorist": "^1.8.0",
+ "local-pkg": "^1.0.0",
+ "mlly": "^1.7.4"
+ }
+ },
+ "node_modules/@iconify/utils/node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3529,6 +3620,14 @@
"react": ">=16"
}
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz",
+ "integrity": "sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==",
+ "dependencies": {
+ "langium": "3.0.0"
+ }
+ },
"node_modules/@microsoft/fast-element": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz",
@@ -6484,6 +6583,228 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ=="
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -6515,6 +6836,11 @@
"@types/estree": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -6726,6 +7052,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -7358,7 +7690,6 @@
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -8312,6 +8643,30 @@
"node": ">= 16"
}
},
+ "node_modules/chevrotain": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
+ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "11.0.3",
+ "@chevrotain/gast": "11.0.3",
+ "@chevrotain/regexp-to-ast": "11.0.3",
+ "@chevrotain/types": "11.0.3",
+ "@chevrotain/utils": "11.0.3",
+ "lodash-es": "4.17.21"
+ }
+ },
+ "node_modules/chevrotain-allstar": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
+ "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ },
+ "peerDependencies": {
+ "chevrotain": "^11.0.0"
+ }
+ },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -8864,6 +9219,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -8871,6 +9234,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="
+ },
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -8908,6 +9276,14 @@
"url": "https://opencollective.com/core-js"
}
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -9054,6 +9430,463 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/cytoscape": {
+ "version": "3.31.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.1.tgz",
+ "integrity": "sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz",
+ "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -9130,6 +9963,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+ },
"node_modules/debounce": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz",
@@ -9279,6 +10117,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -9397,6 +10243,14 @@
"node": ">=12"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
+ "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -11586,6 +12440,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="
+ },
"node_modules/harmony-reflect": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
@@ -12041,7 +12900,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -12179,6 +13037,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
@@ -14058,6 +14924,29 @@
"node": ">=4.0"
}
},
+ "node_modules/katex": {
+ "version": "0.16.21",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
+ "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -14068,6 +14957,11 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -14078,6 +14972,26 @@
"node": ">=6"
}
},
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
+ },
+ "node_modules/langium": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz",
+ "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==",
+ "dependencies": {
+ "chevrotain": "~11.0.3",
+ "chevrotain-allstar": "~0.3.0",
+ "vscode-languageserver": "~9.0.1",
+ "vscode-languageserver-textdocument": "~1.0.11",
+ "vscode-uri": "~3.0.8"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -14098,6 +15012,11 @@
"node": ">=0.10"
}
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -14357,6 +15276,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/local-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.0.tgz",
+ "integrity": "sha512-xbZBuX6gYIWrlLmZG43aAVer4ocntYO09vPy9lxd6Ns8DnR4U7N+IIeDkubinqFOHHzoMlPxTxwo0jhE7oYjAw==",
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^1.3.1",
+ "quansync": "^0.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -14380,6 +15315,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -14550,6 +15490,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "13.0.3",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz",
+ "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -15869,6 +16820,33 @@
"node": ">= 8"
}
},
+ "node_modules/mermaid": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz",
+ "integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.0.1",
+ "@iconify/utils": "^2.1.32",
+ "@mermaid-js/parser": "^0.3.0",
+ "@types/d3": "^7.4.3",
+ "cytoscape": "^3.29.2",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.11",
+ "dayjs": "^1.11.10",
+ "dompurify": "^3.2.1",
+ "katex": "^0.16.9",
+ "khroma": "^2.1.0",
+ "lodash-es": "^4.17.21",
+ "marked": "^13.0.2",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.1",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^9.0.1"
+ }
+ },
"node_modules/micromark": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz",
@@ -16507,6 +17485,17 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mlly": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
+ "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.0",
+ "ufo": "^1.5.4"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -16872,6 +17861,14 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
+ "node_modules/package-manager-detector": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.10.tgz",
+ "integrity": "sha512-1wlNZK7HW+UE3eGCcMv3hDaYokhspuIeH6enXSnCL1eEZSVDsy/dYwo/4CczhUsrKLA1SSXB+qce8Glw5DEVtw==",
+ "dependencies": {
+ "quansync": "^0.2.2"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -16935,6 +17932,11 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -17006,6 +18008,11 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
+ },
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
@@ -17116,6 +18123,30 @@
"node": ">=8"
}
},
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
"node_modules/polished": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
@@ -17307,6 +18338,21 @@
],
"license": "MIT"
},
+ "node_modules/quansync": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.6.tgz",
+ "integrity": "sha512-u3TuxVTuJtkTxKGk5oZ7K2/o+l0/cC6J8SOyaaSnrnroqvcVy7xBxtvBUyd+Xa8cGoCr87XmQj4NR6W+zbqH8w==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ]
+ },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -18434,6 +19480,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+ },
"node_modules/rollup": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
@@ -18472,6 +19523,17 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
@@ -18505,6 +19567,11 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -18564,7 +19631,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
"license": "MIT"
},
"node_modules/saxes": {
@@ -19441,6 +20507,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="
+ },
"node_modules/tinyrainbow": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
@@ -19557,7 +20628,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.10"
@@ -19813,6 +20883,11 @@
"node": ">=4.2.0"
}
},
+ "node_modules/ufo": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
+ "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -20260,7 +21335,6 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
- "dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -20397,6 +21471,49 @@
}
}
},
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="
+ },
"node_modules/vscrui": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/vscrui/-/vscrui-0.2.2.tgz",
diff --git a/webview-ui/package.json b/webview-ui/package.json
index 2206fb35c94..12786d1060f 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -35,6 +35,7 @@
"fast-deep-equal": "^3.1.3",
"fzf": "^0.5.2",
"lucide-react": "^0.475.0",
+ "mermaid": "^11.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.3",
diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx
index 8f391506672..8252d480c6e 100644
--- a/webview-ui/src/components/common/MarkdownBlock.tsx
+++ b/webview-ui/src/components/common/MarkdownBlock.tsx
@@ -1,10 +1,11 @@
-import { memo, useEffect } from "react"
+import React, { memo, useEffect } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
+import MermaidBlock from "./MermaidBlock"
interface MarkdownBlockProps {
markdown?: string
@@ -182,7 +183,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
- pre: ({ node, ...preProps }: any) => ,
+ pre: ({ node, children, ...preProps }: any) => {
+ if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
+ const child = children[0] as React.ReactElement<{ className?: string }>
+ if (child.props?.className?.includes("language-mermaid")) {
+ return child
+ }
+ }
+ return (
+
+ {children}
+
+ )
+ },
+ code: (props: any) => {
+ const className = props.className || ""
+ if (className.includes("language-mermaid")) {
+ const codeText = String(props.children || "")
+ return
+ }
+ return
+ },
},
},
})
diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx
new file mode 100644
index 00000000000..619188179ed
--- /dev/null
+++ b/webview-ui/src/components/common/MermaidBlock.tsx
@@ -0,0 +1,227 @@
+import { useEffect, useRef, useState } from "react"
+import mermaid from "mermaid"
+import { useDebounceEffect } from "../../utils/useDebounceEffect"
+import styled from "styled-components"
+import { vscode } from "../../utils/vscode"
+
+const MERMAID_THEME = {
+ background: "#1e1e1e", // VS Code dark theme background
+ textColor: "#ffffff", // Main text color
+ mainBkg: "#2d2d2d", // Background for nodes
+ nodeBorder: "#888888", // Border color for nodes
+ lineColor: "#cccccc", // Lines connecting nodes
+ primaryColor: "#3c3c3c", // Primary color for highlights
+ primaryTextColor: "#ffffff", // Text in primary colored elements
+ primaryBorderColor: "#888888",
+ secondaryColor: "#2d2d2d", // Secondary color for alternate elements
+ tertiaryColor: "#454545", // Third color for special elements
+
+ // Class diagram specific
+ classText: "#ffffff",
+
+ // State diagram specific
+ labelColor: "#ffffff",
+
+ // Sequence diagram specific
+ actorLineColor: "#cccccc",
+ actorBkg: "#2d2d2d",
+ actorBorder: "#888888",
+ actorTextColor: "#ffffff",
+
+ // Flow diagram specific
+ fillType0: "#2d2d2d",
+ fillType1: "#3c3c3c",
+ fillType2: "#454545",
+}
+
+mermaid.initialize({
+ startOnLoad: false,
+ securityLevel: "loose",
+ theme: "dark",
+ themeVariables: {
+ ...MERMAID_THEME,
+ fontSize: "16px",
+ fontFamily: "var(--vscode-font-family, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif)",
+
+ // Additional styling
+ noteTextColor: "#ffffff",
+ noteBkgColor: "#454545",
+ noteBorderColor: "#888888",
+
+ // Improve contrast for special elements
+ critBorderColor: "#ff9580",
+ critBkgColor: "#803d36",
+
+ // Task diagram specific
+ taskTextColor: "#ffffff",
+ taskTextOutsideColor: "#ffffff",
+ taskTextLightColor: "#ffffff",
+
+ // Numbers/sections
+ sectionBkgColor: "#2d2d2d",
+ sectionBkgColor2: "#3c3c3c",
+
+ // Alt sections in sequence diagrams
+ altBackground: "#2d2d2d",
+
+ // Links
+ linkColor: "#6cb6ff",
+
+ // Borders and lines
+ compositeBackground: "#2d2d2d",
+ compositeBorder: "#888888",
+ titleColor: "#ffffff",
+ },
+})
+
+interface MermaidBlockProps {
+ code: string
+}
+
+export default function MermaidBlock({ code }: MermaidBlockProps) {
+ const containerRef = useRef(null)
+ const [isLoading, setIsLoading] = useState(false)
+
+ // 1) Whenever `code` changes, mark that we need to re-render a new chart
+ useEffect(() => {
+ setIsLoading(true)
+ }, [code])
+
+ // 2) Debounce the actual parse/render
+ useDebounceEffect(
+ () => {
+ if (containerRef.current) {
+ containerRef.current.innerHTML = ""
+ }
+ mermaid
+ .parse(code, { suppressErrors: true })
+ .then((isValid) => {
+ if (!isValid) {
+ throw new Error("Invalid or incomplete Mermaid code")
+ }
+ const id = `mermaid-${Math.random().toString(36).substring(2)}`
+ return mermaid.render(id, code)
+ })
+ .then(({ svg }) => {
+ if (containerRef.current) {
+ containerRef.current.innerHTML = svg
+ }
+ })
+ .catch((err) => {
+ console.warn("Mermaid parse/render failed:", err)
+ containerRef.current!.innerHTML = code.replace(//g, ">")
+ })
+ .finally(() => {
+ setIsLoading(false)
+ })
+ },
+ 500, // Delay 500ms
+ [code], // Dependencies for scheduling
+ )
+
+ /**
+ * Called when user clicks the rendered diagram.
+ * Converts the to a PNG and sends it to the extension.
+ */
+ const handleClick = async () => {
+ if (!containerRef.current) return
+ const svgEl = containerRef.current.querySelector("svg")
+ if (!svgEl) return
+
+ try {
+ const pngDataUrl = await svgToPng(svgEl)
+ vscode.postMessage({
+ type: "openImage",
+ text: pngDataUrl,
+ })
+ } catch (err) {
+ console.error("Error converting SVG to PNG:", err)
+ }
+ }
+
+ return (
+
+ {isLoading && Generating mermaid diagram... }
+
+ {/* The container for the final or raw code. */}
+
+
+ )
+}
+
+async function svgToPng(svgEl: SVGElement): Promise {
+ console.log("svgToPng function called")
+ // Clone the SVG to avoid modifying the original
+ const svgClone = svgEl.cloneNode(true) as SVGElement
+
+ // Get the original viewBox
+ const viewBox = svgClone.getAttribute("viewBox")?.split(" ").map(Number) || []
+ const originalWidth = viewBox[2] || svgClone.clientWidth
+ const originalHeight = viewBox[3] || svgClone.clientHeight
+
+ // Calculate the scale factor to fit editor width while maintaining aspect ratio
+
+ // Unless we can find a way to get the actual editor window dimensions through the VS Code API (which might be possible but would require changes to the extension side),
+ // the fixed width seems like a reliable approach.
+ const editorWidth = 3_600
+
+ const scale = editorWidth / originalWidth
+ const scaledHeight = originalHeight * scale
+
+ // Update SVG dimensions
+ svgClone.setAttribute("width", `${editorWidth}`)
+ svgClone.setAttribute("height", `${scaledHeight}`)
+
+ const serializer = new XMLSerializer()
+ const svgString = serializer.serializeToString(svgClone)
+ const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
+
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.onload = () => {
+ const canvas = document.createElement("canvas")
+ canvas.width = editorWidth
+ canvas.height = scaledHeight
+
+ const ctx = canvas.getContext("2d")
+ if (!ctx) return reject("Canvas context not available")
+
+ // Fill background with Mermaid's dark theme background color
+ ctx.fillStyle = MERMAID_THEME.background
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
+
+ ctx.imageSmoothingEnabled = true
+ ctx.imageSmoothingQuality = "high"
+
+ ctx.drawImage(img, 0, 0, editorWidth, scaledHeight)
+ resolve(canvas.toDataURL("image/png", 1.0))
+ }
+ img.onerror = reject
+ img.src = svgDataUrl
+ })
+}
+
+const MermaidBlockContainer = styled.div`
+ position: relative;
+ margin: 8px 0;
+`
+
+const LoadingMessage = styled.div`
+ padding: 8px 0;
+ color: var(--vscode-descriptionForeground);
+ font-style: italic;
+ font-size: 0.9em;
+`
+
+interface SvgContainerProps {
+ $isLoading: boolean
+}
+
+const SvgContainer = styled.div`
+ opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
+ min-height: 20px;
+ transition: opacity 0.2s ease;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+`
diff --git a/webview-ui/src/utils/useDebounceEffect.ts b/webview-ui/src/utils/useDebounceEffect.ts
new file mode 100644
index 00000000000..b1374ff68d1
--- /dev/null
+++ b/webview-ui/src/utils/useDebounceEffect.ts
@@ -0,0 +1,42 @@
+import { useEffect, useRef } from "react"
+
+type VoidFn = () => void
+
+/**
+ * Runs `effectRef.current()` after `delay` ms whenever any of the `deps` change,
+ * but cancels/re-schedules if they change again before the delay.
+ */
+export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
+ const callbackRef = useRef(effect)
+ const timeoutRef = useRef(null)
+
+ // Keep callbackRef current
+ useEffect(() => {
+ callbackRef.current = effect
+ }, [effect])
+
+ useEffect(() => {
+ // Clear any queued call
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+
+ // Schedule a new call
+ timeoutRef.current = setTimeout(() => {
+ // always call the *latest* version of effect
+ callbackRef.current()
+ }, delay)
+
+ // Cleanup on unmount or next effect
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+ }
+
+ // We want to re‐schedule if any item in `deps` changed,
+ // or if `delay` changed.
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [delay, ...deps])
+}
From 65a2a2cbd16fd2f36f37ba5e675d9a11c899dfd7 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 18:10:33 -0500
Subject: [PATCH 073/541] Update architect prompt
---
src/shared/modes.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/shared/modes.ts b/src/shared/modes.ts
index 2eda966519c..0f6aae64a21 100644
--- a/src/shared/modes.ts
+++ b/src/shared/modes.ts
@@ -92,7 +92,7 @@ export const modes: readonly ModeConfig[] = [
"You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
customInstructions:
- "Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.",
+ "1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.",
},
{
slug: "ask",
From c8b72fa730b0c9a204167ed24b071b1ad0f05ce3 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 18:19:16 -0500
Subject: [PATCH 074/541] Update ask prompt
---
src/shared/modes.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/shared/modes.ts b/src/shared/modes.ts
index 0f6aae64a21..33257a70137 100644
--- a/src/shared/modes.ts
+++ b/src/shared/modes.ts
@@ -101,7 +101,7 @@ export const modes: readonly ModeConfig[] = [
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.",
groups: ["read", "browser", "mcp"],
customInstructions:
- "You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.",
+ "You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.",
},
{
slug: "debug",
From 45eef8e37f823d44816313f13bc69ad208119493 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 18:46:35 -0500
Subject: [PATCH 075/541] Update snapshots
---
.../__tests__/__snapshots__/system.test.ts.snap | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap
index b8050adb77a..291745fcd13 100644
--- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap
@@ -3899,9 +3899,17 @@ USER'S CUSTOM INSTRUCTIONS
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
Mode-specific Instructions:
-Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)
+1. Do some information gathering (for example using read_file or search_files) to get more context about the task.
-Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.
+2. You should also ask the user clarifying questions to get a better understanding of the task.
+
+3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.
+
+4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
+
+5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.
+
+6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.
Rules:
# Rules from .clinerules-architect:
@@ -4176,7 +4184,7 @@ USER'S CUSTOM INSTRUCTIONS
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
Mode-specific Instructions:
-You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.
+You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.
Rules:
# Rules from .clinerules-ask:
From e6a49ddc5c5154d82e9c20dbfe18d5dc99ee7ff2 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sat, 1 Mar 2025 20:42:10 -0500
Subject: [PATCH 076/541] Add type='module' to webview script
---
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 7b6f2c89719..e25f46bde33 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -509,7 +509,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
You need to enable JavaScript to run this app.
-
+