diff --git a/apps/vscode-e2e/src/suite/markdown-lists.test.ts b/apps/vscode-e2e/src/suite/markdown-lists.test.ts index a229d9c2700..9b5c1bd8457 100644 --- a/apps/vscode-e2e/src/suite/markdown-lists.test.ts +++ b/apps/vscode-e2e/src/suite/markdown-lists.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitUntilCompleted } from "./utils" import { setDefaultSuiteTimeout } from "./test-utils" @@ -13,7 +13,7 @@ suite("Markdown List Rendering", function () { const messages: ClineMessage[] = [] - api.on("message", ({ message }: { message: ClineMessage }) => { + api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => { if (message.type === "say" && message.partial === false) { messages.push(message) } @@ -50,7 +50,7 @@ suite("Markdown List Rendering", function () { const messages: ClineMessage[] = [] - api.on("message", ({ message }: { message: ClineMessage }) => { + api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => { if (message.type === "say" && message.partial === false) { messages.push(message) } @@ -87,7 +87,7 @@ suite("Markdown List Rendering", function () { const messages: ClineMessage[] = [] - api.on("message", ({ message }: { message: ClineMessage }) => { + api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => { if (message.type === "say" && message.partial === false) { messages.push(message) } @@ -139,7 +139,7 @@ suite("Markdown List Rendering", function () { const messages: ClineMessage[] = [] - api.on("message", ({ message }: { message: ClineMessage }) => { + api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => { if (message.type === "say" && message.partial === false) { messages.push(message) } diff --git a/apps/vscode-e2e/src/suite/modes.test.ts b/apps/vscode-e2e/src/suite/modes.test.ts index 81d8a2b7fbf..7982f3cf22b 100644 --- a/apps/vscode-e2e/src/suite/modes.test.ts +++ b/apps/vscode-e2e/src/suite/modes.test.ts @@ -1,5 +1,7 @@ import * as assert from "assert" +import { RooCodeEventName } from "@roo-code/types" + import { waitUntilCompleted } from "./utils" import { setDefaultSuiteTimeout } from "./test-utils" @@ -9,7 +11,7 @@ suite("Roo Code Modes", function () { test("Should handle switching modes correctly", async () => { const modes: string[] = [] - globalThis.api.on("taskModeSwitched", (_taskId, mode) => modes.push(mode)) + globalThis.api.on(RooCodeEventName.TaskModeSwitched, (_taskId, mode) => modes.push(mode)) const switchModesTaskId = await globalThis.api.startNewTask({ configuration: { mode: "code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index adf1b2be898..e3e3457520c 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { sleep, waitFor, waitUntilCompleted } from "./utils" @@ -10,7 +10,7 @@ suite.skip("Roo Code Subtasks", () => { const messages: Record = {} - api.on("message", ({ taskId, message }) => { + api.on(RooCodeEventName.Message, ({ taskId, message }) => { if (message.type === "say" && message.partial === false) { messages[taskId] = messages[taskId] || [] messages[taskId].push(message) @@ -37,7 +37,7 @@ suite.skip("Roo Code Subtasks", () => { let spawnedTaskId: string | undefined = undefined // Wait for the subtask to be spawned and then cancel it. - api.on("taskSpawned", (_, childTaskId) => (spawnedTaskId = childTaskId)) + api.on(RooCodeEventName.TaskSpawned, (_, childTaskId) => (spawnedTaskId = childTaskId)) await waitFor(() => !!spawnedTaskId) await sleep(1_000) // Give the task a chance to start and populate the history. await api.cancelCurrentTask() diff --git a/apps/vscode-e2e/src/suite/task.test.ts b/apps/vscode-e2e/src/suite/task.test.ts index 31e03271b52..10e4e4f9a62 100644 --- a/apps/vscode-e2e/src/suite/task.test.ts +++ b/apps/vscode-e2e/src/suite/task.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitUntilCompleted } from "./utils" import { setDefaultSuiteTimeout } from "./test-utils" @@ -13,7 +13,7 @@ suite("Roo Code Task", function () { const messages: ClineMessage[] = [] - api.on("message", ({ message }) => { + api.on(RooCodeEventName.Message, ({ message }) => { if (message.type === "say" && message.partial === false) { messages.push(message) } diff --git a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts b/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts index 6e6dbc59951..729d6839b19 100644 --- a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts +++ b/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -192,7 +192,7 @@ function validateInput(input) { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -201,7 +201,7 @@ function validateInput(input) { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -209,7 +209,7 @@ function validateInput(input) { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -260,9 +260,9 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, console.log("Test passed! apply_diff tool executed and file modified successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -305,7 +305,7 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -314,7 +314,7 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -322,7 +322,7 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -375,9 +375,9 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, console.log("Test passed! apply_diff tool executed and multiple replacements applied successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -424,7 +424,7 @@ function keepThis() { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -432,14 +432,14 @@ function keepThis() { taskStarted = true } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -487,9 +487,9 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, console.log("Test passed! apply_diff tool executed and targeted modification successful") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -532,7 +532,7 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -540,14 +540,14 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`, taskStarted = true } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -598,9 +598,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! apply_diff attempted and error handled gracefully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -663,7 +663,7 @@ function checkInput(input) { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -672,7 +672,7 @@ function checkInput(input) { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -680,7 +680,7 @@ function checkInput(input) { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -742,9 +742,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! apply_diff tool executed and multiple search/replace blocks applied successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/execute-command.test.ts b/apps/vscode-e2e/src/suite/tools/execute-command.test.ts index 21933d0879f..f207dae685c 100644 --- a/apps/vscode-e2e/src/suite/tools/execute-command.test.ts +++ b/apps/vscode-e2e/src/suite/tools/execute-command.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep, waitUntilCompleted } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -145,7 +145,7 @@ suite("Roo Code execute_command Tool", function () { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -154,7 +154,7 @@ suite("Roo Code execute_command Tool", function () { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -162,7 +162,7 @@ suite("Roo Code execute_command Tool", function () { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -208,9 +208,9 @@ Then use the attempt_completion tool to complete the task. Do not suggest any co console.log("Test passed! Command executed successfully") } finally { // Clean up event listeners - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -251,7 +251,7 @@ Then use the attempt_completion tool to complete the task. Do not suggest any co } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -260,7 +260,7 @@ Then use the attempt_completion tool to complete the task. Do not suggest any co console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -268,7 +268,7 @@ Then use the attempt_completion tool to complete the task. Do not suggest any co console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -320,9 +320,9 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`, console.log("Test passed! Command executed in custom directory") } finally { // Clean up event listeners - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) // Clean up subdirectory try { @@ -365,7 +365,7 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`, } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -374,7 +374,7 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`, console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -382,7 +382,7 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`, console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -440,9 +440,9 @@ After both commands are executed, use the attempt_completion tool to complete th console.log("Test passed! Multiple commands executed successfully") } finally { // Clean up event listeners - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -484,7 +484,7 @@ After both commands are executed, use the attempt_completion tool to complete th } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -493,7 +493,7 @@ After both commands are executed, use the attempt_completion tool to complete th console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -501,7 +501,7 @@ After both commands are executed, use the attempt_completion tool to complete th console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -550,9 +550,9 @@ Avoid at all costs suggesting a command when using the attempt_completion tool`, console.log("Test passed! Long-running command handled successfully") } finally { // Clean up event listeners - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/insert-content.test.ts b/apps/vscode-e2e/src/suite/tools/insert-content.test.ts index c9d65d0d0be..4dd0c209280 100644 --- a/apps/vscode-e2e/src/suite/tools/insert-content.test.ts +++ b/apps/vscode-e2e/src/suite/tools/insert-content.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -145,7 +145,7 @@ ${testFile.content}` } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -154,7 +154,7 @@ ${testFile.content}` console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -162,7 +162,7 @@ ${testFile.content}` console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -221,9 +221,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! insert_content tool executed and content inserted at beginning successfully") } finally { - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) try { @@ -286,7 +286,7 @@ ${insertContent}` } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -295,7 +295,7 @@ ${insertContent}` console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -303,7 +303,7 @@ ${insertContent}` console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -388,7 +388,7 @@ ${testFile.content}` } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -397,7 +397,7 @@ ${testFile.content}` console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -405,7 +405,7 @@ ${testFile.content}` console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -490,7 +490,7 @@ And this is the second line` } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -499,7 +499,7 @@ And this is the second line` console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -507,7 +507,7 @@ And this is the second line` console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -572,9 +572,9 @@ The file is currently empty. Assume the file exists and you can modify it direct "Test passed! insert_content tool executed and content inserted into empty file successfully", ) } finally { - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) // Check if the file was modified correctly @@ -600,9 +600,9 @@ The file is currently empty. Assume the file exists and you can modify it direct console.log("Test passed! insert_content tool executed and multiline content inserted successfully") } finally { - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) assert.strictEqual(insertContentExecuted, true, "insert_content tool should have been executed") @@ -619,9 +619,9 @@ The file is currently empty. Assume the file exists and you can modify it direct console.log("Test passed! insert_content tool executed and content inserted at end successfully") } finally { - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) // Tests will be added here one by one diff --git a/apps/vscode-e2e/src/suite/tools/list-files.test.ts b/apps/vscode-e2e/src/suite/tools/list-files.test.ts index c374e795159..5a1fd6cc3be 100644 --- a/apps/vscode-e2e/src/suite/tools/list-files.test.ts +++ b/apps/vscode-e2e/src/suite/tools/list-files.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -207,7 +207,7 @@ This directory contains various files and subdirectories for testing the list_fi } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -215,7 +215,7 @@ This directory contains various files and subdirectories for testing the list_fi taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -271,8 +271,8 @@ This directory contains various files and subdirectories for testing the list_fi console.log("Test passed! Directory listing (non-recursive) executed successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -310,7 +310,7 @@ This directory contains various files and subdirectories for testing the list_fi } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -318,7 +318,7 @@ This directory contains various files and subdirectories for testing the list_fi taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -381,8 +381,8 @@ This directory contains various files and subdirectories for testing the list_fi console.log("Test passed! Directory listing (recursive) executed successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -420,7 +420,7 @@ This directory contains various files and subdirectories for testing the list_fi } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -428,7 +428,7 @@ This directory contains various files and subdirectories for testing the list_fi taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -499,8 +499,8 @@ This directory contains various files and subdirectories for testing the list_fi await fs.rm(testDir, { recursive: true, force: true }) } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -523,7 +523,7 @@ This directory contains various files and subdirectories for testing the list_fi } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -531,7 +531,7 @@ This directory contains various files and subdirectories for testing the list_fi taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -569,8 +569,8 @@ This directory contains various files and subdirectories for testing the list_fi console.log("Test passed! Workspace root directory listing executed successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/read-file.test.ts b/apps/vscode-e2e/src/suite/tools/read-file.test.ts index 007e88b21c8..99e3f184577 100644 --- a/apps/vscode-e2e/src/suite/tools/read-file.test.ts +++ b/apps/vscode-e2e/src/suite/tools/read-file.test.ts @@ -4,7 +4,7 @@ import * as path from "path" import * as os from "os" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -180,7 +180,7 @@ suite("Roo Code read_file Tool", function () { console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -189,7 +189,7 @@ suite("Roo Code read_file Tool", function () { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -197,7 +197,7 @@ suite("Roo Code read_file Tool", function () { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -259,9 +259,9 @@ suite("Roo Code read_file Tool", function () { console.log("Test passed! File read successfully with correct content") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -314,7 +314,7 @@ suite("Roo Code read_file Tool", function () { console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -322,7 +322,7 @@ suite("Roo Code read_file Tool", function () { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -371,8 +371,8 @@ suite("Roo Code read_file Tool", function () { console.log("Test passed! Multiline file read successfully with correct content") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -425,7 +425,7 @@ suite("Roo Code read_file Tool", function () { console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -433,7 +433,7 @@ suite("Roo Code read_file Tool", function () { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -484,8 +484,8 @@ suite("Roo Code read_file Tool", function () { console.log("Test passed! File read with line range successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -512,7 +512,7 @@ suite("Roo Code read_file Tool", function () { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -520,7 +520,7 @@ suite("Roo Code read_file Tool", function () { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -556,8 +556,8 @@ suite("Roo Code read_file Tool", function () { console.log("Test passed! Non-existent file handled correctly") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -585,7 +585,7 @@ suite("Roo Code read_file Tool", function () { console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -593,7 +593,7 @@ suite("Roo Code read_file Tool", function () { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -627,8 +627,8 @@ suite("Roo Code read_file Tool", function () { console.log("Test passed! XML file read successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -651,7 +651,7 @@ suite("Roo Code read_file Tool", function () { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -659,7 +659,7 @@ suite("Roo Code read_file Tool", function () { taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -700,8 +700,8 @@ Assume both files exist and you can read them directly. Read each file and tell console.log("Test passed! Multiple files read successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -729,7 +729,7 @@ Assume both files exist and you can read them directly. Read each file and tell console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -737,7 +737,7 @@ Assume both files exist and you can read them directly. Read each file and tell taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -771,8 +771,8 @@ Assume both files exist and you can read them directly. Read each file and tell console.log("Test passed! Large file read efficiently") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts b/apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts index 459b1093501..801a829a74b 100644 --- a/apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts +++ b/apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -175,7 +175,7 @@ Final content`, } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -184,7 +184,7 @@ Final content`, console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -192,7 +192,7 @@ Final content`, console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -249,9 +249,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! search_and_replace tool executed and file modified successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -303,7 +303,7 @@ function anotherNewFunction() { } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -312,7 +312,7 @@ function anotherNewFunction() { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -320,7 +320,7 @@ function anotherNewFunction() { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -378,9 +378,9 @@ Use the search_and_replace tool twice - once for each replacement.`, console.log("Test passed! search_and_replace tool executed with regex successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -429,7 +429,7 @@ Final content` } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -438,7 +438,7 @@ Final content` console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -446,7 +446,7 @@ Final content` console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -503,9 +503,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! search_and_replace tool executed and replaced multiple matches successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -549,7 +549,7 @@ Assume the file exists and you can modify it directly.`, } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -558,7 +558,7 @@ Assume the file exists and you can modify it directly.`, console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -566,7 +566,7 @@ Assume the file exists and you can modify it directly.`, console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -623,9 +623,9 @@ Assume the file exists and you can modify it directly.`, console.log("Test passed! search_and_replace tool executed and handled no matches correctly") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/search-files.test.ts b/apps/vscode-e2e/src/suite/tools/search-files.test.ts index cc28739943e..98cfd1b3eed 100644 --- a/apps/vscode-e2e/src/suite/tools/search-files.test.ts +++ b/apps/vscode-e2e/src/suite/tools/search-files.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -323,7 +323,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -331,7 +331,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -397,8 +397,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! Function definitions found successfully with validated results") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -421,7 +421,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -429,7 +429,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -464,8 +464,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! TODO comments found successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -488,7 +488,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -496,7 +496,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -530,8 +530,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! TypeScript interfaces found with file pattern filter") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -554,7 +554,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -562,7 +562,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -598,8 +598,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! JSON configuration keys found successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -622,7 +622,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -630,7 +630,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -663,8 +663,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! Nested directory search completed successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -690,7 +690,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -698,7 +698,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -731,8 +731,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! Complex regex pattern search completed successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -775,7 +775,7 @@ The search should find matches across different file types and provide context f console.log("AI completion message:", message.text?.substring(0, 300)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -783,7 +783,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -859,8 +859,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! No-match scenario handled correctly") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -883,7 +883,7 @@ The search should find matches across different file types and provide context f } } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -891,7 +891,7 @@ The search should find matches across different file types and provide context f taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -927,8 +927,8 @@ The search should find matches across different file types and provide context f console.log("Test passed! Class definitions and async methods found successfully") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts b/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts index 8e83dd7e4b5..380a77d179e 100644 --- a/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts +++ b/apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts @@ -4,7 +4,7 @@ import * as path from "path" import * as os from "os" import * as vscode from "vscode" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -167,7 +167,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.error("Error:", message.text) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -176,7 +176,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -184,7 +184,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) await sleep(2000) // Wait for Roo Code to fully initialize // Trigger MCP server detection by opening and modifying the file @@ -284,9 +284,9 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP read_file tool used successfully and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -344,7 +344,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.error("Error:", message.text) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -352,7 +352,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { _taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -413,8 +413,8 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP write_file tool used successfully and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -472,7 +472,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.error("Error:", message.text) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -480,7 +480,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { _taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -552,8 +552,8 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP list_directory tool used successfully and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -611,7 +611,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.error("Error:", message.text) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -619,7 +619,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { _taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -691,8 +691,8 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP directory_tree tool used successfully and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -730,7 +730,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Attempt completion called:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -738,7 +738,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { _taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -762,8 +762,8 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP error handling verified and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -832,7 +832,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.error("Error:", message.text) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task completion const taskCompletedHandler = (id: string) => { @@ -840,7 +840,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { _taskCompleted = true } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -921,8 +921,8 @@ suite.skip("Roo Code use_mcp_tool Tool", function () { console.log("Test passed! MCP message format validation successful and task completed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts b/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts index 814213b2bc6..dea51386cf9 100644 --- a/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts +++ b/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs/promises" import * as path from "path" import * as os from "os" -import type { ClineMessage } from "@roo-code/types" +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" import { waitFor, sleep } from "../utils" import { setDefaultSuiteTimeout } from "../test-utils" @@ -110,7 +110,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("AI response:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -119,7 +119,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -127,7 +127,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -259,9 +259,9 @@ suite("Roo Code write_to_file Tool", function () { console.log("write_to_file tool was properly executed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) @@ -302,7 +302,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("Tool request:", message.text?.substring(0, 200)) } } - api.on("message", messageHandler) + api.on(RooCodeEventName.Message, messageHandler) // Listen for task events const taskStartedHandler = (id: string) => { @@ -311,7 +311,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("Task started:", id) } } - api.on("taskStarted", taskStartedHandler) + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) const taskCompletedHandler = (id: string) => { if (id === taskId) { @@ -319,7 +319,7 @@ suite("Roo Code write_to_file Tool", function () { console.log("Task completed:", id) } } - api.on("taskCompleted", taskCompletedHandler) + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) let taskId: string try { @@ -440,9 +440,9 @@ suite("Roo Code write_to_file Tool", function () { console.log("write_to_file tool was properly executed") } finally { // Clean up - api.off("message", messageHandler) - api.off("taskStarted", taskStartedHandler) - api.off("taskCompleted", taskCompletedHandler) + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) } }) }) diff --git a/apps/vscode-e2e/src/suite/utils.ts b/apps/vscode-e2e/src/suite/utils.ts index d41fa9e8ed5..874ded9accc 100644 --- a/apps/vscode-e2e/src/suite/utils.ts +++ b/apps/vscode-e2e/src/suite/utils.ts @@ -1,4 +1,4 @@ -import type { RooCodeAPI } from "@roo-code/types" +import { RooCodeEventName, type RooCodeAPI } from "@roo-code/types" type WaitForOptions = { timeout?: number @@ -46,7 +46,7 @@ type WaitUntilAbortedOptions = WaitForOptions & { export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbortedOptions) => { const set = new Set() - api.on("taskAborted", (taskId) => set.add(taskId)) + api.on(RooCodeEventName.TaskAborted, (taskId) => set.add(taskId)) await waitFor(() => set.has(taskId), options) } @@ -57,7 +57,7 @@ type WaitUntilCompletedOptions = WaitForOptions & { export const waitUntilCompleted = async ({ api, taskId, ...options }: WaitUntilCompletedOptions) => { const set = new Set() - api.on("taskCompleted", (taskId) => set.add(taskId)) + api.on(RooCodeEventName.TaskCompleted, (taskId) => set.add(taskId)) await waitFor(() => set.has(taskId), options) } diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts new file mode 100644 index 00000000000..52c3c2521d7 --- /dev/null +++ b/packages/cloud/src/CloudAPI.ts @@ -0,0 +1,122 @@ +import { type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types" + +import { getRooCodeApiUrl } from "./config" +import type { AuthService } from "./auth" +import { getUserAgent } from "./utils" +import { AuthenticationError, CloudAPIError, NetworkError, TaskNotFoundError } from "./errors" + +interface CloudAPIRequestOptions extends Omit { + timeout?: number + headers?: Record +} + +export class CloudAPI { + private authService: AuthService + private log: (...args: unknown[]) => void + private baseUrl: string + + constructor(authService: AuthService, log?: (...args: unknown[]) => void) { + this.authService = authService + this.log = log || console.log + this.baseUrl = getRooCodeApiUrl() + } + + private async request( + endpoint: string, + options: CloudAPIRequestOptions & { + parseResponse?: (data: unknown) => T + } = {}, + ): Promise { + const { timeout = 10000, parseResponse, headers = {}, ...fetchOptions } = options + + const sessionToken = this.authService.getSessionToken() + + if (!sessionToken) { + throw new AuthenticationError() + } + + const url = `${this.baseUrl}${endpoint}` + + const requestHeaders = { + "Content-Type": "application/json", + Authorization: `Bearer ${sessionToken}`, + "User-Agent": getUserAgent(), + ...headers, + } + + try { + const response = await fetch(url, { + ...fetchOptions, + headers: requestHeaders, + signal: AbortSignal.timeout(timeout), + }) + + if (!response.ok) { + await this.handleErrorResponse(response, endpoint) + } + + const data = await response.json() + + if (parseResponse) { + return parseResponse(data) + } + + return data as T + } catch (error) { + if (error instanceof TypeError && error.message.includes("fetch")) { + throw new NetworkError(`Network error while calling ${endpoint}`) + } + + if (error instanceof CloudAPIError) { + throw error + } + + if (error instanceof Error && error.name === "AbortError") { + throw new CloudAPIError(`Request to ${endpoint} timed out`, undefined, undefined) + } + + throw new CloudAPIError( + `Unexpected error while calling ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + + private async handleErrorResponse(response: Response, endpoint: string): Promise { + let responseBody: unknown + + try { + responseBody = await response.json() + } catch { + responseBody = await response.text() + } + + switch (response.status) { + case 401: + throw new AuthenticationError() + case 404: + if (endpoint.includes("/share")) { + throw new TaskNotFoundError() + } + throw new CloudAPIError(`Resource not found: ${endpoint}`, 404, responseBody) + default: + throw new CloudAPIError( + `HTTP ${response.status}: ${response.statusText}`, + response.status, + responseBody, + ) + } + } + + async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise { + this.log(`[CloudAPI] Sharing task ${taskId} with visibility: ${visibility}`) + + const response = await this.request("/api/extension/share", { + method: "POST", + body: JSON.stringify({ taskId, visibility }), + parseResponse: (data) => shareResponseSchema.parse(data), + }) + + this.log("[CloudAPI] Share response:", response) + return response + } +} diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index ff33671a40d..7777d6b220e 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -12,13 +12,15 @@ import type { import { TelemetryService } from "@roo-code/telemetry" import { CloudServiceEvents } from "./types" +import { TaskNotFoundError } from "./errors" import type { AuthService } from "./auth" import { WebAuthService, StaticTokenAuthService } from "./auth" import type { SettingsService } from "./SettingsService" import { CloudSettingsService } from "./CloudSettingsService" import { StaticSettingsService } from "./StaticSettingsService" import { TelemetryClient } from "./TelemetryClient" -import { ShareService, TaskNotFoundError } from "./ShareService" +import { CloudShareService } from "./CloudShareService" +import { CloudAPI } from "./CloudAPI" type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0] type AuthUserInfoPayload = CloudServiceEvents["user-info"][0] @@ -34,7 +36,8 @@ export class CloudService extends EventEmitter implements vs private settingsListener: (data: SettingsPayload) => void private settingsService: SettingsService | null = null private telemetryClient: TelemetryClient | null = null - private shareService: ShareService | null = null + private shareService: CloudShareService | null = null + private cloudAPI: CloudAPI | null = null private isInitialized = false private log: (...args: unknown[]) => void @@ -87,8 +90,9 @@ export class CloudService extends EventEmitter implements vs this.settingsService = cloudSettingsService } + this.cloudAPI = new CloudAPI(this.authService, this.log) this.telemetryClient = new TelemetryClient(this.authService, this.settingsService) - this.shareService = new ShareService(this.authService, this.settingsService, this.log) + this.shareService = new CloudShareService(this.cloudAPI, this.settingsService, this.log) try { TelemetryService.instance.register(this.telemetryClient) @@ -209,7 +213,7 @@ export class CloudService extends EventEmitter implements vs return await this.shareService!.shareTask(taskId, visibility) } catch (error) { if (error instanceof TaskNotFoundError && clineMessages) { - // Backfill messages and retry + // Backfill messages and retry. await this.telemetryClient!.backfillMessages(clineMessages, taskId) return await this.shareService!.shareTask(taskId, visibility) } @@ -229,6 +233,7 @@ export class CloudService extends EventEmitter implements vs this.authService.off("auth-state-changed", this.authStateListener) this.authService.off("user-info", this.authUserInfoListener) } + if (this.settingsService) { if (this.settingsService instanceof CloudSettingsService) { this.settingsService.off("settings-updated", this.settingsListener) diff --git a/packages/cloud/src/CloudSettingsService.ts b/packages/cloud/src/CloudSettingsService.ts index 4ce52774db0..c842d800fc5 100644 --- a/packages/cloud/src/CloudSettingsService.ts +++ b/packages/cloud/src/CloudSettingsService.ts @@ -8,7 +8,7 @@ import { organizationSettingsSchema, } from "@roo-code/types" -import { getRooCodeApiUrl } from "./Config" +import { getRooCodeApiUrl } from "./config" import type { AuthService, AuthState } from "./auth" import { RefreshTimer } from "./RefreshTimer" import type { SettingsService } from "./SettingsService" diff --git a/packages/cloud/src/CloudShareService.ts b/packages/cloud/src/CloudShareService.ts new file mode 100644 index 00000000000..91e0f6aa3fb --- /dev/null +++ b/packages/cloud/src/CloudShareService.ts @@ -0,0 +1,43 @@ +import * as vscode from "vscode" + +import type { ShareResponse, ShareVisibility } from "@roo-code/types" + +import type { CloudAPI } from "./CloudAPI" +import type { SettingsService } from "./SettingsService" + +export class CloudShareService { + private cloudAPI: CloudAPI + private settingsService: SettingsService + private log: (...args: unknown[]) => void + + constructor(cloudAPI: CloudAPI, settingsService: SettingsService, log?: (...args: unknown[]) => void) { + this.cloudAPI = cloudAPI + this.settingsService = settingsService + this.log = log || console.log + } + + async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise { + try { + const response = await this.cloudAPI.shareTask(taskId, visibility) + + if (response.success && response.shareUrl) { + // Copy to clipboard. + await vscode.env.clipboard.writeText(response.shareUrl) + } + + return response + } catch (error) { + this.log("[ShareService] Error sharing task:", error) + throw error + } + } + + async canShareTask(): Promise { + try { + return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing + } catch (error) { + this.log("[ShareService] Error checking if task can be shared:", error) + return false + } + } +} diff --git a/packages/cloud/src/ShareService.ts b/packages/cloud/src/ShareService.ts deleted file mode 100644 index 5dcc7cae3f8..00000000000 --- a/packages/cloud/src/ShareService.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as vscode from "vscode" - -import { shareResponseSchema } from "@roo-code/types" -import { getRooCodeApiUrl } from "./Config" -import type { AuthService } from "./auth" -import type { SettingsService } from "./SettingsService" -import { getUserAgent } from "./utils" - -export type ShareVisibility = "organization" | "public" - -export class TaskNotFoundError extends Error { - constructor(taskId?: string) { - super(taskId ? `Task '${taskId}' not found` : "Task not found") - Object.setPrototypeOf(this, TaskNotFoundError.prototype) - } -} - -export class ShareService { - private authService: AuthService - private settingsService: SettingsService - private log: (...args: unknown[]) => void - - constructor(authService: AuthService, settingsService: SettingsService, log?: (...args: unknown[]) => void) { - this.authService = authService - this.settingsService = settingsService - this.log = log || console.log - } - - /** - * Share a task with specified visibility - * Returns the share response data - */ - async shareTask(taskId: string, visibility: ShareVisibility = "organization") { - try { - const sessionToken = this.authService.getSessionToken() - if (!sessionToken) { - throw new Error("Authentication required") - } - - const response = await fetch(`${getRooCodeApiUrl()}/api/extension/share`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - "User-Agent": getUserAgent(), - }, - body: JSON.stringify({ taskId, visibility }), - signal: AbortSignal.timeout(10000), - }) - - if (!response.ok) { - if (response.status === 404) { - throw new TaskNotFoundError(taskId) - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data = shareResponseSchema.parse(await response.json()) - this.log("[share] Share link created successfully:", data) - - if (data.success && data.shareUrl) { - // Copy to clipboard - await vscode.env.clipboard.writeText(data.shareUrl) - } - - return data - } catch (error) { - this.log("[share] Error sharing task:", error) - throw error - } - } - - /** - * Check if sharing is available - */ - async canShareTask(): Promise { - try { - if (!this.authService.isAuthenticated()) { - return false - } - - return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing - } catch (error) { - this.log("[share] Error checking if task can be shared:", error) - return false - } - } -} diff --git a/packages/cloud/src/StaticSettingsService.ts b/packages/cloud/src/StaticSettingsService.ts index 3aac37bda5e..97e6cf7ea83 100644 --- a/packages/cloud/src/StaticSettingsService.ts +++ b/packages/cloud/src/StaticSettingsService.ts @@ -36,6 +36,6 @@ export class StaticSettingsService implements SettingsService { } public dispose(): void { - // No resources to clean up for static settings + // No resources to clean up for static settings. } } diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts index e33843a30c6..727da034325 100644 --- a/packages/cloud/src/TelemetryClient.ts +++ b/packages/cloud/src/TelemetryClient.ts @@ -6,7 +6,7 @@ import { } from "@roo-code/types" import { BaseTelemetryClient } from "@roo-code/telemetry" -import { getRooCodeApiUrl } from "./Config" +import { getRooCodeApiUrl } from "./config" import type { AuthService } from "./auth" import type { SettingsService } from "./SettingsService" diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index fd3ae9b9c03..607b21de342 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -1,14 +1,16 @@ // npx vitest run src/__tests__/CloudService.test.ts import * as vscode from "vscode" + import type { ClineMessage } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "../CloudService" import { WebAuthService } from "../auth/WebAuthService" import { CloudSettingsService } from "../CloudSettingsService" -import { ShareService, TaskNotFoundError } from "../ShareService" +import { CloudShareService } from "../CloudShareService" import { TelemetryClient } from "../TelemetryClient" -import { TelemetryService } from "@roo-code/telemetry" +import { TaskNotFoundError } from "../errors" vi.mock("vscode", () => ({ ExtensionContext: vi.fn(), @@ -30,7 +32,7 @@ vi.mock("../auth/WebAuthService") vi.mock("../CloudSettingsService") -vi.mock("../ShareService") +vi.mock("../CloudShareService") vi.mock("../TelemetryClient") @@ -154,7 +156,7 @@ describe("CloudService", () => { vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService) vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService) - vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService) + vi.mocked(CloudShareService).mockImplementation(() => mockShareService as unknown as CloudShareService) vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient) vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) diff --git a/packages/cloud/src/__tests__/CloudSettingsService.test.ts b/packages/cloud/src/__tests__/CloudSettingsService.test.ts index e9d0ae3c931..4a85383ba40 100644 --- a/packages/cloud/src/__tests__/CloudSettingsService.test.ts +++ b/packages/cloud/src/__tests__/CloudSettingsService.test.ts @@ -6,8 +6,8 @@ import type { OrganizationSettings } from "@roo-code/types" // Mock dependencies vi.mock("../RefreshTimer") -vi.mock("../Config", () => ({ - getRooCodeApiUrl: vi.fn().mockReturnValue("https://api.example.com"), +vi.mock("../config", () => ({ + getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) // Mock fetch globally @@ -338,7 +338,7 @@ describe("CloudSettingsService", () => { const result = await timerCallback() expect(result).toBe(true) - expect(fetch).toHaveBeenCalledWith("https://api.example.com/api/organization-settings", { + expect(fetch).toHaveBeenCalledWith("https://app.roocode.com/api/organization-settings", { headers: { Authorization: "Bearer valid-token", }, diff --git a/packages/cloud/src/__tests__/ShareService.test.ts b/packages/cloud/src/__tests__/CloudShareService.test.ts similarity index 86% rename from packages/cloud/src/__tests__/ShareService.test.ts rename to packages/cloud/src/__tests__/CloudShareService.test.ts index dd5b6696033..6fae1fbb9fb 100644 --- a/packages/cloud/src/__tests__/ShareService.test.ts +++ b/packages/cloud/src/__tests__/CloudShareService.test.ts @@ -3,9 +3,11 @@ import type { MockedFunction } from "vitest" import * as vscode from "vscode" -import { ShareService, TaskNotFoundError } from "../ShareService" -import type { AuthService } from "../auth" +import { CloudAPI } from "../CloudAPI" +import { CloudShareService } from "../CloudShareService" import type { SettingsService } from "../SettingsService" +import type { AuthService } from "../auth" +import { CloudAPIError, TaskNotFoundError } from "../errors" // Mock fetch const mockFetch = vi.fn() @@ -44,10 +46,11 @@ vi.mock("../utils", () => ({ getUserAgent: () => "Roo-Code 1.0.0", })) -describe("ShareService", () => { - let shareService: ShareService +describe("CloudShareService", () => { + let shareService: CloudShareService let mockAuthService: AuthService let mockSettingsService: SettingsService + let mockCloudAPI: CloudAPI let mockLog: MockedFunction<(...args: unknown[]) => void> beforeEach(() => { @@ -65,7 +68,8 @@ describe("ShareService", () => { getSettings: vi.fn(), } as any - shareService = new ShareService(mockAuthService, mockSettingsService, mockLog) + mockCloudAPI = new CloudAPI(mockAuthService, mockLog) + shareService = new CloudShareService(mockCloudAPI, mockSettingsService, mockLog) }) describe("shareTask", () => { @@ -189,12 +193,12 @@ describe("ShareService", () => { ok: false, status: 404, statusText: "Not Found", + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + text: vi.fn().mockResolvedValue("Not Found"), }) await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError) - await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( - "Task 'task-123' not found", - ) + await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Task not found") }) it("should throw generic Error for non-404 HTTP errors", async () => { @@ -203,12 +207,14 @@ describe("ShareService", () => { ok: false, status: 500, statusText: "Internal Server Error", + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + text: vi.fn().mockResolvedValue("Internal Server Error"), }) + await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(CloudAPIError) await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow( "HTTP 500: Internal Server Error", ) - await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError) }) it("should create TaskNotFoundError with correct properties", async () => { @@ -217,6 +223,8 @@ describe("ShareService", () => { ok: false, status: 404, statusText: "Not Found", + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + text: vi.fn().mockResolvedValue("Not Found"), }) try { @@ -225,7 +233,7 @@ describe("ShareService", () => { } catch (error) { expect(error).toBeInstanceOf(TaskNotFoundError) expect(error).toBeInstanceOf(Error) - expect((error as TaskNotFoundError).message).toBe("Task 'task-123' not found") + expect((error as TaskNotFoundError).message).toBe("Task not found") } }) }) @@ -277,8 +285,8 @@ describe("ShareService", () => { expect(result).toBe(false) }) - it("should return false when not authenticated", async () => { - ;(mockAuthService.isAuthenticated as any).mockReturnValue(false) + it("should return false when settings service returns undefined", async () => { + ;(mockSettingsService.getSettings as any).mockReturnValue(undefined) const result = await shareService.canShareTask() @@ -286,13 +294,17 @@ describe("ShareService", () => { }) it("should handle errors gracefully", async () => { - ;(mockAuthService.isAuthenticated as any).mockImplementation(() => { - throw new Error("Auth error") + ;(mockSettingsService.getSettings as any).mockImplementation(() => { + throw new Error("Settings error") }) const result = await shareService.canShareTask() expect(result).toBe(false) + expect(mockLog).toHaveBeenCalledWith( + "[ShareService] Error checking if task can be shared:", + expect.any(Error), + ) }) }) }) diff --git a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts index 457e1d706d3..82fd964b7f2 100644 --- a/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts +++ b/packages/cloud/src/__tests__/auth/WebAuthService.spec.ts @@ -1,17 +1,17 @@ -// npx vitest run src/__tests__/AuthService.spec.ts +// npx vitest run src/__tests__/auth/WebAuthService.spec.ts -import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest" +import { type Mock } from "vitest" import crypto from "crypto" import * as vscode from "vscode" import { WebAuthService } from "../../auth/WebAuthService" import { RefreshTimer } from "../../RefreshTimer" -import * as Config from "../../Config" -import * as utils from "../../utils" +import { getClerkBaseUrl, getRooCodeApiUrl } from "../../config" +import { getUserAgent } from "../../utils" // Mock external dependencies vi.mock("../../RefreshTimer") -vi.mock("../../Config") +vi.mock("../../config") vi.mock("../../utils") vi.mock("crypto") @@ -101,11 +101,11 @@ describe("WebAuthService", () => { MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer) // Setup config mocks - use production URL by default to maintain existing test behavior - vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") - vi.mocked(Config.getRooCodeApiUrl).mockReturnValue("https://api.test.com") + vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") + vi.mocked(getRooCodeApiUrl).mockReturnValue("https://api.test.com") // Setup utils mock - vi.mocked(utils.getUserAgent).mockReturnValue("Roo-Code 1.0.0") + vi.mocked(getUserAgent).mockReturnValue("Roo-Code 1.0.0") // Setup crypto mock vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never) @@ -977,7 +977,7 @@ describe("WebAuthService", () => { describe("auth credentials key scoping", () => { it("should use default key when getClerkBaseUrl returns production URL", async () => { // Mock getClerkBaseUrl to return production URL - vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") + vi.mocked(getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com") const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } @@ -994,7 +994,7 @@ describe("WebAuthService", () => { it("should use scoped key when getClerkBaseUrl returns custom URL", async () => { const customUrl = "https://custom.clerk.com" // Mock getClerkBaseUrl to return custom URL - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } @@ -1010,7 +1010,7 @@ describe("WebAuthService", () => { it("should load credentials using scoped key", async () => { const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) const credentials = { clientToken: "test-token", sessionId: "test-session" } @@ -1025,7 +1025,7 @@ describe("WebAuthService", () => { it("should clear credentials using scoped key", async () => { const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog) @@ -1037,7 +1037,7 @@ describe("WebAuthService", () => { it("should listen for changes on scoped key", async () => { const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) let onDidChangeCallback: (e: { key: string }) => void @@ -1064,7 +1064,7 @@ describe("WebAuthService", () => { it("should not respond to changes on different scoped keys", async () => { const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) let onDidChangeCallback: (e: { key: string }) => void @@ -1088,7 +1088,7 @@ describe("WebAuthService", () => { it("should not respond to changes on default key when using scoped key", async () => { const customUrl = "https://custom.clerk.com" - vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl) + vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl) let onDidChangeCallback: (e: { key: string }) => void diff --git a/packages/cloud/src/auth/AuthService.ts b/packages/cloud/src/auth/AuthService.ts index 57e026d72a7..a49ad0104d4 100644 --- a/packages/cloud/src/auth/AuthService.ts +++ b/packages/cloud/src/auth/AuthService.ts @@ -1,4 +1,5 @@ import EventEmitter from "events" + import type { CloudUserInfo } from "@roo-code/types" export interface AuthServiceEvents { diff --git a/packages/cloud/src/auth/StaticTokenAuthService.ts b/packages/cloud/src/auth/StaticTokenAuthService.ts index 507f82c9f67..04821006d52 100644 --- a/packages/cloud/src/auth/StaticTokenAuthService.ts +++ b/packages/cloud/src/auth/StaticTokenAuthService.ts @@ -1,6 +1,9 @@ import EventEmitter from "events" + import * as vscode from "vscode" + import type { CloudUserInfo } from "@roo-code/types" + import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" export class StaticTokenAuthService extends EventEmitter implements AuthService { diff --git a/packages/cloud/src/auth/WebAuthService.ts b/packages/cloud/src/auth/WebAuthService.ts index 8fd892f44f3..b94957950b6 100644 --- a/packages/cloud/src/auth/WebAuthService.ts +++ b/packages/cloud/src/auth/WebAuthService.ts @@ -6,11 +6,19 @@ import { z } from "zod" import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types" -import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../Config" -import { RefreshTimer } from "../RefreshTimer" +import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../config" import { getUserAgent } from "../utils" +import { InvalidClientTokenError } from "../errors" +import { RefreshTimer } from "../RefreshTimer" + import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService" +const AUTH_STATE_KEY = "clerk-auth-state" + +/** + * AuthCredentials + */ + const authCredentialsSchema = z.object({ clientToken: z.string().min(1, "Client token cannot be empty"), sessionId: z.string().min(1, "Session ID cannot be empty"), @@ -19,7 +27,9 @@ const authCredentialsSchema = z.object({ type AuthCredentials = z.infer -const AUTH_STATE_KEY = "clerk-auth-state" +/** + * Clerk Schemas + */ const clerkSignInResponseSchema = z.object({ response: z.object({ @@ -33,8 +43,9 @@ const clerkCreateSessionTokenResponseSchema = z.object({ const clerkMeResponseSchema = z.object({ response: z.object({ - first_name: z.string().optional().nullable(), - last_name: z.string().optional().nullable(), + id: z.string().optional(), + first_name: z.string().nullish(), + last_name: z.string().nullish(), image_url: z.string().optional(), primary_email_address_id: z.string().optional(), email_addresses: z @@ -69,13 +80,6 @@ const clerkOrganizationMembershipsSchema = z.object({ ), }) -class InvalidClientTokenError extends Error { - constructor() { - super("Invalid/Expired client token") - Object.setPrototypeOf(this, InvalidClientTokenError.prototype) - } -} - export class WebAuthService extends EventEmitter implements AuthService { private context: vscode.ExtensionContext private timer: RefreshTimer @@ -94,8 +98,9 @@ export class WebAuthService extends EventEmitter implements A this.context = context this.log = log || console.log - // Calculate auth credentials key based on Clerk base URL + // Calculate auth credentials key based on Clerk base URL. const clerkBaseUrl = getClerkBaseUrl() + if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) { this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}` } else { @@ -514,9 +519,13 @@ export class WebAuthService extends EventEmitter implements A throw new Error(`HTTP ${response.status}: ${response.statusText}`) } - const { response: userData } = clerkMeResponseSchema.parse(await response.json()) + const payload = await response.json() + const { response: userData } = clerkMeResponseSchema.parse(payload) - const userInfo: CloudUserInfo = {} + const userInfo: CloudUserInfo = { + id: userData.id, + picture: userData.image_url, + } const names = [userData.first_name, userData.last_name].filter((name) => !!name) userInfo.name = names.length > 0 ? names.join(" ") : undefined @@ -529,8 +538,6 @@ export class WebAuthService extends EventEmitter implements A )?.email_address } - userInfo.picture = userData.image_url - // Fetch organization info if user is in organization context try { const storedOrgId = this.getStoredOrganizationId() @@ -544,6 +551,7 @@ export class WebAuthService extends EventEmitter implements A if (userMembership) { this.setUserOrganizationInfo(userInfo, userMembership) + this.log("[auth] User in organization context:", { id: userMembership.organization.id, name: userMembership.organization.name, @@ -562,6 +570,7 @@ export class WebAuthService extends EventEmitter implements A if (primaryOrgMembership) { this.setUserOrganizationInfo(userInfo, primaryOrgMembership) + this.log("[auth] Legacy credentials: Found organization membership:", { id: primaryOrgMembership.organization.id, name: primaryOrgMembership.organization.name, diff --git a/packages/cloud/src/Config.ts b/packages/cloud/src/config.ts similarity index 81% rename from packages/cloud/src/Config.ts rename to packages/cloud/src/config.ts index 08b0cc7a188..e682d718cea 100644 --- a/packages/cloud/src/Config.ts +++ b/packages/cloud/src/config.ts @@ -1,7 +1,5 @@ -// Production constants export const PRODUCTION_CLERK_BASE_URL = "https://clerk.roocode.com" export const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com" -// Functions with environment variable fallbacks export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_URL diff --git a/packages/cloud/src/errors.ts b/packages/cloud/src/errors.ts new file mode 100644 index 00000000000..7400f26b39c --- /dev/null +++ b/packages/cloud/src/errors.ts @@ -0,0 +1,42 @@ +export class CloudAPIError extends Error { + constructor( + message: string, + public statusCode?: number, + public responseBody?: unknown, + ) { + super(message) + this.name = "CloudAPIError" + Object.setPrototypeOf(this, CloudAPIError.prototype) + } +} + +export class TaskNotFoundError extends CloudAPIError { + constructor(taskId?: string) { + super(taskId ? `Task '${taskId}' not found` : "Task not found", 404) + this.name = "TaskNotFoundError" + Object.setPrototypeOf(this, TaskNotFoundError.prototype) + } +} + +export class AuthenticationError extends CloudAPIError { + constructor(message = "Authentication required") { + super(message, 401) + this.name = "AuthenticationError" + Object.setPrototypeOf(this, AuthenticationError.prototype) + } +} + +export class NetworkError extends CloudAPIError { + constructor(message = "Network error occurred") { + super(message) + this.name = "NetworkError" + Object.setPrototypeOf(this, NetworkError.prototype) + } +} + +export class InvalidClientTokenError extends Error { + constructor() { + super("Invalid/Expired client token") + Object.setPrototypeOf(this, InvalidClientTokenError.prototype) + } +} diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts index 9770f349c6f..55f7d908dda 100644 --- a/packages/cloud/src/index.ts +++ b/packages/cloud/src/index.ts @@ -1,2 +1,4 @@ +export * from "./config" + +export * from "./CloudAPI" export * from "./CloudService" -export * from "./Config" diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 6fb181b5734..e61e1e61067 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -1,27 +1,12 @@ import type { EventEmitter } from "events" import type { Socket } from "net" +import type { RooCodeEvents } from "./events.js" import type { RooCodeSettings } from "./global-settings.js" import type { ProviderSettingsEntry, ProviderSettings } from "./provider-settings.js" -import type { ClineMessage, TokenUsage } from "./message.js" -import type { ToolUsage, ToolName } from "./tool.js" -import type { IpcMessage, IpcServerEvents, IsSubtask } from "./ipc.js" +import type { IpcMessage, IpcServerEvents } from "./ipc.js" -// TODO: Make sure this matches `RooCodeEvents` from `@roo-code/types`. -export interface RooCodeAPIEvents { - message: [data: { taskId: string; action: "created" | "updated"; message: ClineMessage }] - taskCreated: [taskId: string] - taskStarted: [taskId: string] - taskModeSwitched: [taskId: string, mode: string] - taskPaused: [taskId: string] - taskUnpaused: [taskId: string] - taskAskResponded: [taskId: string] - taskAborted: [taskId: string] - taskSpawned: [parentTaskId: string, childTaskId: string] - taskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage, isSubtask: IsSubtask] - taskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage] - taskToolFailed: [taskId: string, toolName: ToolName, error: string] -} +export type RooCodeAPIEvents = RooCodeEvents export interface RooCodeAPI extends EventEmitter { /** diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index c8acc2bcae9..a4eb9f96a87 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -9,6 +9,7 @@ import { discriminatedProviderSettingsWithIdSchema } from "./provider-settings.j */ export interface CloudUserInfo { + id?: string name?: string email?: string picture?: string diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts new file mode 100644 index 00000000000..42c389ab60f --- /dev/null +++ b/packages/types/src/events.ts @@ -0,0 +1,192 @@ +import { z } from "zod" + +import { clineMessageSchema, tokenUsageSchema } from "./message.js" +import { toolNamesSchema, toolUsageSchema } from "./tool.js" + +/** + * RooCodeEventName + */ + +export enum RooCodeEventName { + // Task Provider Lifecycle + TaskCreated = "taskCreated", + + // Task Lifecycle + TaskStarted = "taskStarted", + TaskCompleted = "taskCompleted", + TaskAborted = "taskAborted", + TaskFocused = "taskFocused", + TaskUnfocused = "taskUnfocused", + TaskActive = "taskActive", + TaskIdle = "taskIdle", + + // Subtask Lifecycle + TaskPaused = "taskPaused", + TaskUnpaused = "taskUnpaused", + TaskSpawned = "taskSpawned", + + // Task Execution + Message = "message", + TaskModeSwitched = "taskModeSwitched", + TaskAskResponded = "taskAskResponded", + + // Task Analytics + TaskTokenUsageUpdated = "taskTokenUsageUpdated", + TaskToolFailed = "taskToolFailed", + + // Evals + EvalPass = "evalPass", + EvalFail = "evalFail", +} + +/** + * RooCodeEvents + */ + +export const rooCodeEventsSchema = z.object({ + [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), + + [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), + [RooCodeEventName.TaskCompleted]: z.tuple([ + z.string(), + tokenUsageSchema, + toolUsageSchema, + z.object({ + isSubtask: z.boolean(), + }), + ]), + [RooCodeEventName.TaskAborted]: z.tuple([z.string()]), + [RooCodeEventName.TaskFocused]: z.tuple([z.string()]), + [RooCodeEventName.TaskUnfocused]: z.tuple([z.string()]), + [RooCodeEventName.TaskActive]: z.tuple([z.string()]), + [RooCodeEventName.TaskIdle]: z.tuple([z.string()]), + + [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), + [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), + [RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]), + + [RooCodeEventName.Message]: z.tuple([ + z.object({ + taskId: z.string(), + action: z.union([z.literal("created"), z.literal("updated")]), + message: clineMessageSchema, + }), + ]), + [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), + [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), + + [RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]), + [RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]), +}) + +export type RooCodeEvents = z.infer + +/** + * TaskEvent + */ + +export const taskEventSchema = z.discriminatedUnion("eventName", [ + // Task Provider Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskCreated), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated], + taskId: z.number().optional(), + }), + + // Task Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskStarted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskCompleted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskAborted), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskFocused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskFocused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskUnfocused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnfocused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskActive), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskActive], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskIdle), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskIdle], + taskId: z.number().optional(), + }), + + // Subtask Lifecycle + z.object({ + eventName: z.literal(RooCodeEventName.TaskPaused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskUnpaused), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskSpawned), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned], + taskId: z.number().optional(), + }), + + // Task Execution + z.object({ + eventName: z.literal(RooCodeEventName.Message), + payload: rooCodeEventsSchema.shape[RooCodeEventName.Message], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskModeSwitched), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskAskResponded), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded], + taskId: z.number().optional(), + }), + + // Task Analytics + z.object({ + eventName: z.literal(RooCodeEventName.TaskToolFailed), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskToolFailed], + taskId: z.number().optional(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated), + payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated], + taskId: z.number().optional(), + }), + + // Evals + z.object({ + eventName: z.literal(RooCodeEventName.EvalPass), + payload: z.undefined(), + taskId: z.number(), + }), + z.object({ + eventName: z.literal(RooCodeEventName.EvalFail), + payload: z.undefined(), + taskId: z.number(), + }), +]) + +export type TaskEvent = z.infer diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 44937da235b..dcbb1c4f54f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,7 @@ -export * from "./providers/index.js" - export * from "./api.js" -export * from "./codebase-index.js" export * from "./cloud.js" +export * from "./codebase-index.js" +export * from "./events.js" export * from "./experiment.js" export * from "./followup.js" export * from "./global-settings.js" @@ -15,9 +14,12 @@ export * from "./mode.js" export * from "./model.js" export * from "./provider-settings.js" export * from "./sharing.js" +export * from "./task.js" +export * from "./todo.js" export * from "./telemetry.js" export * from "./terminal.js" export * from "./tool.js" export * from "./type-fu.js" export * from "./vscode.js" -export * from "./todo.js" + +export * from "./providers/index.js" diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 28accde9de5..22cba1dea83 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -1,61 +1,29 @@ import { z } from "zod" -import { clineMessageSchema, tokenUsageSchema } from "./message.js" -import { toolNamesSchema, toolUsageSchema } from "./tool.js" +import { type TaskEvent, taskEventSchema } from "./events.js" import { rooCodeSettingsSchema } from "./global-settings.js" /** - * isSubtaskSchema + * IpcMessageType */ -export const isSubtaskSchema = z.object({ - isSubtask: z.boolean(), -}) -export type IsSubtask = z.infer + +export enum IpcMessageType { + Connect = "Connect", + Disconnect = "Disconnect", + Ack = "Ack", + TaskCommand = "TaskCommand", + TaskEvent = "TaskEvent", +} /** - * RooCodeEvent + * IpcOrigin */ -export enum RooCodeEventName { - Message = "message", - TaskCreated = "taskCreated", - TaskStarted = "taskStarted", - TaskModeSwitched = "taskModeSwitched", - TaskPaused = "taskPaused", - TaskUnpaused = "taskUnpaused", - TaskAskResponded = "taskAskResponded", - TaskAborted = "taskAborted", - TaskSpawned = "taskSpawned", - TaskCompleted = "taskCompleted", - TaskTokenUsageUpdated = "taskTokenUsageUpdated", - TaskToolFailed = "taskToolFailed", - EvalPass = "evalPass", - EvalFail = "evalFail", +export enum IpcOrigin { + Client = "client", + Server = "server", } -export const rooCodeEventsSchema = z.object({ - [RooCodeEventName.Message]: z.tuple([ - z.object({ - taskId: z.string(), - action: z.union([z.literal("created"), z.literal("updated")]), - message: clineMessageSchema, - }), - ]), - [RooCodeEventName.TaskCreated]: z.tuple([z.string()]), - [RooCodeEventName.TaskStarted]: z.tuple([z.string()]), - [RooCodeEventName.TaskModeSwitched]: z.tuple([z.string(), z.string()]), - [RooCodeEventName.TaskPaused]: z.tuple([z.string()]), - [RooCodeEventName.TaskUnpaused]: z.tuple([z.string()]), - [RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]), - [RooCodeEventName.TaskAborted]: z.tuple([z.string()]), - [RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]), - [RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema, isSubtaskSchema]), - [RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]), - [RooCodeEventName.TaskToolFailed]: z.tuple([z.string(), toolNamesSchema, z.string()]), -}) - -export type RooCodeEvents = z.infer - /** * Ack */ @@ -69,7 +37,7 @@ export const ackSchema = z.object({ export type Ack = z.infer /** - * TaskCommand + * TaskCommandName */ export enum TaskCommandName { @@ -78,6 +46,10 @@ export enum TaskCommandName { CloseTask = "CloseTask", } +/** + * TaskCommand + */ + export const taskCommandSchema = z.discriminatedUnion("commandName", [ z.object({ commandName: z.literal(TaskCommandName.StartNewTask), @@ -100,102 +72,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ export type TaskCommand = z.infer -/** - * TaskEvent - */ - -export const taskEventSchema = z.discriminatedUnion("eventName", [ - z.object({ - eventName: z.literal(RooCodeEventName.Message), - payload: rooCodeEventsSchema.shape[RooCodeEventName.Message], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskCreated), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCreated], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskStarted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskStarted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskModeSwitched), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskModeSwitched], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskPaused), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskPaused], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskUnpaused), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskUnpaused], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskAskResponded), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAskResponded], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskAborted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskAborted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskSpawned), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskSpawned], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskCompleted), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskCompleted], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskTokenUsageUpdated), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskTokenUsageUpdated], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.TaskToolFailed), - payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskToolFailed], - taskId: z.number().optional(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.EvalPass), - payload: z.undefined(), - taskId: z.number(), - }), - z.object({ - eventName: z.literal(RooCodeEventName.EvalFail), - payload: z.undefined(), - taskId: z.number(), - }), -]) - -export type TaskEvent = z.infer - /** * IpcMessage */ -export enum IpcMessageType { - Connect = "Connect", - Disconnect = "Disconnect", - Ack = "Ack", - TaskCommand = "TaskCommand", - TaskEvent = "TaskEvent", -} - -export enum IpcOrigin { - Client = "client", - Server = "server", -} - export const ipcMessageSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(IpcMessageType.Ack), @@ -219,7 +99,7 @@ export const ipcMessageSchema = z.discriminatedUnion("type", [ export type IpcMessage = z.infer /** - * Client + * IpcClientEvents */ export type IpcClientEvents = { @@ -231,7 +111,7 @@ export type IpcClientEvents = { } /** - * Server + * IpcServerEvents */ export type IpcServerEvents = { diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index eaec2ad8865..21baf3f2033 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -44,6 +44,26 @@ export const clineAskSchema = z.enum(clineAsks) export type ClineAsk = z.infer +/** + * BlockingAsk + */ + +export const blockingAsks: ClineAsk[] = [ + "api_req_failed", + "mistake_limit_reached", + "completion_result", + "resume_task", + "resume_completed_task", + "command_output", + "auto_approval_max_req_reached", +] as const + +export type BlockingAsk = (typeof blockingAsks)[number] + +export function isBlockingAsk(ask: ClineAsk): ask is BlockingAsk { + return blockingAsks.includes(ask) +} + /** * ClineSay */ diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts new file mode 100644 index 00000000000..4da1a1f6f58 --- /dev/null +++ b/packages/types/src/task.ts @@ -0,0 +1,98 @@ +import { RooCodeEventName } from "./events.js" +import { type ClineMessage, type BlockingAsk, type TokenUsage } from "./message.js" +import { type ToolUsage, type ToolName } from "./tool.js" + +/** + * TaskProviderLike + */ + +export interface TaskProviderState { + mode?: string +} + +export interface TaskProviderLike { + readonly cwd: string + + getCurrentCline(): TaskLike | undefined + getCurrentTaskStack(): string[] + + initClineWithTask(text?: string, images?: string[], parentTask?: TaskLike): Promise + cancelTask(): Promise + clearTask(): Promise + postStateToWebview(): Promise + + getState(): Promise + + postMessageToWebview(message: unknown): Promise + + on( + event: K, + listener: (...args: TaskProviderEvents[K]) => void | Promise, + ): this + + off( + event: K, + listener: (...args: TaskProviderEvents[K]) => void | Promise, + ): this + + context: { + extension?: { + packageJSON?: { + version?: string + } + } + } +} + +export type TaskProviderEvents = { + [RooCodeEventName.TaskCreated]: [task: TaskLike] + + // Proxied from the Task EventEmitter. + [RooCodeEventName.TaskStarted]: [taskId: string] + [RooCodeEventName.TaskCompleted]: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] + [RooCodeEventName.TaskAborted]: [taskId: string] + [RooCodeEventName.TaskFocused]: [taskId: string] + [RooCodeEventName.TaskUnfocused]: [taskId: string] + [RooCodeEventName.TaskActive]: [taskId: string] + [RooCodeEventName.TaskIdle]: [taskId: string] +} + +/** + * TaskLike + */ + +export interface TaskLike { + readonly taskId: string + readonly rootTask?: TaskLike + readonly blockingAsk?: BlockingAsk + + on(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this + off(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this + + setMessageResponse(text: string, images?: string[]): void +} + +export type TaskEvents = { + // Task Lifecycle + [RooCodeEventName.TaskStarted]: [] + [RooCodeEventName.TaskCompleted]: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] + [RooCodeEventName.TaskAborted]: [] + [RooCodeEventName.TaskFocused]: [] + [RooCodeEventName.TaskUnfocused]: [] + [RooCodeEventName.TaskActive]: [taskId: string] + [RooCodeEventName.TaskIdle]: [taskId: string] + + // Subtask Lifecycle + [RooCodeEventName.TaskPaused]: [] + [RooCodeEventName.TaskUnpaused]: [] + [RooCodeEventName.TaskSpawned]: [taskId: string] + + // Task Execution + [RooCodeEventName.Message]: [{ action: "created" | "updated"; message: ClineMessage }] + [RooCodeEventName.TaskModeSwitched]: [taskId: string, mode: string] + [RooCodeEventName.TaskAskResponded]: [] + + // Task Analytics + [RooCodeEventName.TaskToolFailed]: [taskId: string, tool: ToolName, error: string] + [RooCodeEventName.TaskTokenUsageUpdated]: [taskId: string, tokenUsage: TokenUsage] +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 9df9a225d11..84797b815c1 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -10,21 +10,26 @@ import pWaitFor from "p-wait-for" import { serializeError } from "serialize-error" import { + type TaskLike, + type TaskEvents, type ProviderSettings, type TokenUsage, type ToolUsage, type ToolName, type ContextCondense, - type ClineAsk, type ClineMessage, type ClineSay, + type ClineAsk, + type BlockingAsk, type ToolProgressStatus, - DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, type HistoryItem, + RooCodeEventName, TelemetryEventName, TodoItem, getApiProtocol, getModelId, + DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, + isBlockingAsk, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -96,24 +101,6 @@ import { AutoApprovalHandler } from "./AutoApprovalHandler" const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes -export type TaskEvents = { - message: [{ action: "created" | "updated"; message: ClineMessage }] - taskStarted: [] - taskModeSwitched: [taskId: string, mode: string] - taskPaused: [] - taskUnpaused: [] - taskAskResponded: [] - taskAborted: [] - taskSpawned: [taskId: string] - taskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage] - taskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage] - taskToolFailed: [taskId: string, tool: ToolName, error: string] -} - -export type TaskEventHandlers = { - [K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise -} - export type TaskOptions = { provider: ClineProvider apiConfiguration: ProviderSettings @@ -132,7 +119,7 @@ export type TaskOptions = { onCreated?: (task: Task) => void } -export class Task extends EventEmitter { +export class Task extends EventEmitter implements TaskLike { todoList?: TodoItem[] readonly taskId: string readonly instanceId: string @@ -189,6 +176,7 @@ export class Task extends EventEmitter { providerRef: WeakRef private readonly globalStoragePath: string abort: boolean = false + blockingAsk?: BlockingAsk didFinishAbortingStream = false abandoned = false isInitialized = false @@ -545,7 +533,7 @@ export class Task extends EventEmitter { this.clineMessages.push(message) const provider = this.providerRef.deref() await provider?.postStateToWebview() - this.emit("message", { action: "created", message }) + this.emit(RooCodeEventName.Message, { action: "created", message }) await this.saveClineMessages() const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled() @@ -567,7 +555,7 @@ export class Task extends EventEmitter { private async updateClineMessage(message: ClineMessage) { const provider = this.providerRef.deref() await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message }) - this.emit("message", { action: "updated", message }) + this.emit(RooCodeEventName.Message, { action: "updated", message }) const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled() @@ -596,7 +584,7 @@ export class Task extends EventEmitter { mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode }) - this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage) + this.emit(RooCodeEventName.TaskTokenUsageUpdated, this.taskId, tokenUsage) await this.providerRef.deref()?.updateTaskHistory(historyItem) } catch (error) { @@ -702,7 +690,17 @@ export class Task extends EventEmitter { await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected }) } + // Detect if the task will enter an idle state. + const isReady = this.askResponse !== undefined || this.lastMessageTs !== askTs + + if (!partial && !isReady && isBlockingAsk(type)) { + this.blockingAsk = type + this.emit(RooCodeEventName.TaskIdle, this.taskId) + } + + console.log(`[Task#${this.taskId}] pWaitFor askResponse(${type}) -> blocking`) await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 }) + console.log(`[Task#${this.taskId}] pWaitFor askResponse(${type}) -> unblocked (${this.askResponse})`) if (this.lastMessageTs !== askTs) { // Could happen if we send multiple asks in a row i.e. with @@ -715,11 +713,22 @@ export class Task extends EventEmitter { this.askResponse = undefined this.askResponseText = undefined this.askResponseImages = undefined - this.emit("taskAskResponded") + + // Switch back to an active state. + if (this.blockingAsk) { + this.blockingAsk = undefined + this.emit(RooCodeEventName.TaskActive, this.taskId) + } + + this.emit(RooCodeEventName.TaskAskResponded) return result } - async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) { + public setMessageResponse(text: string, images?: string[]) { + this.handleWebviewAskResponse("messageResponse", text, images) + } + + handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) { this.askResponse = askResponse this.askResponseText = text this.askResponseImages = images @@ -947,7 +956,7 @@ export class Task extends EventEmitter { public async resumePausedTask(lastMessage: string) { // Release this Cline instance from paused state. this.isPaused = false - this.emit("taskUnpaused") + this.emit(RooCodeEventName.TaskUnpaused) // 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 @@ -981,7 +990,10 @@ export class Task extends EventEmitter { modifiedClineMessages.splice(lastRelevantMessageIndex + 1) } - // since we don't use api_req_finished anymore, we need to check if the last api_req_started has a cost value, if it doesn't and no cancellation reason to present, then we remove it since it indicates an api request without any partial content streamed + // Since we don't use `api_req_finished` anymore, we need to check if the + // last `api_req_started` has a cost value, if it doesn't and no + // cancellation reason to present, then we remove it since it indicates + // an api request without any partial content streamed. const lastApiReqStartedIndex = findLastIndex( modifiedClineMessages, (m) => m.type === "say" && m.say === "api_req_started", @@ -990,6 +1002,7 @@ export class Task extends EventEmitter { if (lastApiReqStartedIndex !== -1) { const lastApiReqStarted = modifiedClineMessages[lastApiReqStartedIndex] const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}") + if (cost === undefined && cancelReason === undefined) { modifiedClineMessages.splice(lastApiReqStartedIndex, 1) } @@ -1009,7 +1022,7 @@ export class Task extends EventEmitter { const lastClineMessage = this.clineMessages .slice() .reverse() - .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // could be multiple resume tasks + .find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task")) // Could be multiple resume tasks. let askType: ClineAsk if (lastClineMessage?.ask === "completion_result") { @@ -1020,9 +1033,11 @@ export class Task extends EventEmitter { this.isInitialized = true - const { response, text, images } = await this.ask(askType) // calls poststatetowebview + const { response, text, images } = await this.ask(askType) // Calls `postStateToWebview`. + let responseText: string | undefined let responseImages: string[] | undefined + if (response === "messageResponse") { await this.say("user_feedback", text, images) responseText = text @@ -1200,6 +1215,8 @@ export class Task extends EventEmitter { } public dispose(): void { + console.log(`[Task] disposing task ${this.taskId}.${this.instanceId}`) + // Stop waiting for child task completion. if (this.pauseInterval) { clearInterval(this.pauseInterval) @@ -1261,7 +1278,7 @@ export class Task extends EventEmitter { } this.abort = true - this.emit("taskAborted") + this.emit(RooCodeEventName.TaskAborted) try { this.dispose() // Call the centralized dispose method @@ -1303,11 +1320,11 @@ export class Task extends EventEmitter { let nextUserContent = userContent let includeFileDetails = true - this.emit("taskStarted") + this.emit(RooCodeEventName.TaskStarted) while (!this.abort) { const didEndLoop = await this.recursivelyMakeClineRequests(nextUserContent, includeFileDetails) - includeFileDetails = false // we only need file details the first time + includeFileDetails = false // We only need file details the first time. // The way this agentic loop works is that cline will be given a // task that he then calls tools to complete. Unless there's an @@ -1633,13 +1650,13 @@ export class Task extends EventEmitter { // If this.abort is already true, it means the user clicked cancel, so we should // treat this as "user_cancelled" rather than "streaming_failed" const cancelReason = this.abort ? "user_cancelled" : "streaming_failed" + const streamingFailedMessage = this.abort ? undefined : (error.message ?? JSON.stringify(serializeError(error), null, 2)) - // Now call abortTask after determining the cancel reason + // Now call abortTask after determining the cancel reason. await this.abortTask() - await abortStream(cancelReason, streamingFailedMessage) const history = await provider?.getTaskWithId(this.taskId) @@ -2126,7 +2143,7 @@ export class Task extends EventEmitter { this.toolUsage[toolName].failures++ if (error) { - this.emit("taskToolFailed", this.taskId, toolName, error) + this.emit(RooCodeEventName.TaskToolFailed, this.taskId, toolName, error) } } diff --git a/src/core/tools/attemptCompletionTool.ts b/src/core/tools/attemptCompletionTool.ts index ef7881854f6..5074d7f4e80 100644 --- a/src/core/tools/attemptCompletionTool.ts +++ b/src/core/tools/attemptCompletionTool.ts @@ -1,6 +1,7 @@ import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" +import { RooCodeEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../task/Task" @@ -41,11 +42,13 @@ export async function attemptCompletionTool( if (preventCompletionWithOpenTodos && hasIncompleteTodos) { cline.consecutiveMistakeCount++ cline.recordToolError("attempt_completion") + pushToolResult( formatResponse.toolError( "Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.", ), ) + return } @@ -67,12 +70,12 @@ export async function attemptCompletionTool( await cline.say("completion_result", removeClosingTag("result", result), undefined, false) TelemetryService.instance.captureTaskCompleted(cline.taskId) - cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage) + cline.emit(RooCodeEventName.TaskCompleted, cline.taskId, cline.getTokenUsage(), cline.toolUsage) await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) } } else { - // no command, still outputting partial result + // No command, still outputting partial result await cline.say("completion_result", removeClosingTag("result", result), undefined, block.partial) } return @@ -90,7 +93,7 @@ export async function attemptCompletionTool( // Users must use execute_command tool separately before attempt_completion await cline.say("completion_result", result, undefined, false) TelemetryService.instance.captureTaskCompleted(cline.taskId) - cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage) + cline.emit(RooCodeEventName.TaskCompleted, cline.taskId, cline.getTokenUsage(), cline.toolUsage) if (cline.parentTask) { const didApprove = await askFinishSubTaskApproval() diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index cc56659d02b..46a1fe5d9be 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -1,5 +1,7 @@ import delay from "delay" +import { RooCodeEventName } from "@roo-code/types" + import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { Task } from "../task/Task" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" @@ -93,14 +95,14 @@ export async function newTaskTool( // Delay to allow mode change to take effect await delay(500) - cline.emit("taskSpawned", newCline.taskId) + cline.emit(RooCodeEventName.TaskSpawned, newCline.taskId) pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`) // Set the isPaused flag to true so the parent // task can wait for the sub-task to finish. cline.isPaused = true - cline.emit("taskPaused") + cline.emit(RooCodeEventName.TaskPaused) return } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 980eb1f07b9..ed8f8a27d13 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -10,6 +10,8 @@ import pWaitFor from "p-wait-for" import * as vscode from "vscode" import { + type TaskProviderLike, + type TaskProviderEvents, type GlobalState, type ProviderName, type ProviderSettings, @@ -24,6 +26,7 @@ import { type TerminalActionPromptType, type HistoryItem, type CloudUserInfo, + RooCodeEventName, requestyDefaultModelId, openRouterDefaultModelId, glamaDefaultModelId, @@ -34,8 +37,6 @@ import { import { TelemetryService } from "@roo-code/telemetry" import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" -import { t } from "../../i18n" -import { setPanel } from "../../activate/registerCommands" import { Package } from "../../shared/package" import { findLast } from "../../shared/array" import { supportPrompt } from "../../shared/support-prompt" @@ -44,10 +45,15 @@ import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/Ext import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes" import { experimentDefault } from "../../shared/experiments" import { formatLanguage } from "../../shared/language" +import { WebviewMessage } from "../../shared/WebviewMessage" +import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" +import { ProfileValidator } from "../../shared/ProfileValidator" + import { Terminal } from "../../integrations/terminal/Terminal" import { downloadTask } from "../../integrations/misc/export-markdown" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" + import { McpHub } from "../../services/mcp/McpHub" import { McpServerManager } from "../../services/mcp/McpServerManager" import { MarketplaceManager } from "../../services/marketplace" @@ -55,36 +61,37 @@ import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckp import { CodeIndexManager } from "../../services/code-index/manager" import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager" import { MdmService } from "../../services/mdm/MdmService" + import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" +import { getWorkspaceGitInfo } from "../../utils/git" +import { getWorkspacePath } from "../../utils/path" + +import { setPanel } from "../../activate/registerCommands" + +import { t } from "../../i18n" + +import { buildApiHandler } from "../../api" +import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/providers/fetchers/lmstudio" + import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" import { CustomModesManager } from "../config/CustomModesManager" -import { buildApiHandler } from "../../api" import { Task, TaskOptions } from "../task/Task" -import { getNonce } from "./getNonce" -import { getUri } from "./getUri" import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt" -import { getWorkspacePath } from "../../utils/path" + import { webviewMessageHandler } from "./webviewMessageHandler" -import { WebviewMessage } from "../../shared/WebviewMessage" -import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" -import { ProfileValidator } from "../../shared/ProfileValidator" -import { getWorkspaceGitInfo } from "../../utils/git" -import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/providers/fetchers/lmstudio" +import { getNonce } from "./getNonce" +import { getUri } from "./getUri" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts */ -export type ClineProviderEvents = { - taskCreated: [task: Task] -} - export class ClineProvider - extends EventEmitter - implements vscode.WebviewViewProvider, TelemetryPropertiesProvider + extends EventEmitter + implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike { // Used in package.json as the view's id. This value cannot be changed due // to how VSCode caches views based on their id, and updating the id would @@ -155,7 +162,7 @@ export class ClineProvider this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager) - // Initialize cloud profile sync + // Initialize Roo Code Cloud profile sync. this.initializeCloudProfileSync().catch((error) => { this.log(`Failed to initialize cloud profile sync: ${error}`) }) @@ -226,17 +233,18 @@ export class ClineProvider } } - // Adds a new Cline instance to clineStack, marking the start of a new task. + // Adds a new Task 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. - async addClineToStack(cline: Task) { - console.log(`[subtasks] adding task ${cline.taskId}.${cline.instanceId} to stack`) + async addClineToStack(task: Task) { + console.log(`[subtasks] adding task ${task.taskId}.${task.instanceId} to stack`) // Add this cline instance into the stack that represents the order of all the called tasks. - this.clineStack.push(cline) + this.clineStack.push(task) + task.emit(RooCodeEventName.TaskFocused) - // Perform special setup provider specific tasks - await this.performPreparationTasks(cline) + // Perform special setup provider specific tasks. + await this.performPreparationTasks(task) // Ensure getState() resolves correctly. const state = await this.getState() @@ -247,7 +255,8 @@ export class ClineProvider } async performPreparationTasks(cline: Task) { - // LMStudio: we need to force model loading in order to read its context size; we do it now since we're starting a task with that model selected + // LMStudio: We need to force model loading in order to read its context + // size; we do it now since we're starting a task with that model selected. if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") { try { if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) { @@ -271,24 +280,26 @@ export class ClineProvider } // Pop the top Cline instance from the stack. - let cline = this.clineStack.pop() + let task = this.clineStack.pop() - if (cline) { - console.log(`[subtasks] removing task ${cline.taskId}.${cline.instanceId} from stack`) + if (task) { + console.log(`[subtasks] removing task ${task.taskId}.${task.instanceId} from stack`) try { // Abort the running task and set isAbandoned to true so // all running promises will exit as well. - await cline.abortTask(true) + await task.abortTask(true) } catch (e) { this.log( - `[subtasks] encountered error while aborting task ${cline.taskId}.${cline.instanceId}: ${e.message}`, + `[subtasks] encountered error while aborting task ${task.taskId}.${task.instanceId}: ${e.message}`, ) } + task.emit(RooCodeEventName.TaskUnfocused) + // Make sure no reference kept, once promises end it will be // garbage collected. - cline = undefined + task = undefined } } @@ -343,8 +354,13 @@ export class ClineProvider async dispose() { this.log("Disposing ClineProvider...") - await this.removeClineFromStack() - this.log("Cleared task") + + // Clear all tasks from the stack. + while (this.clineStack.length > 0) { + await this.removeClineFromStack() + } + + this.log("Cleared all tasks") if (this.view && "dispose" in this.view) { this.view.dispose() @@ -375,6 +391,9 @@ export class ClineProvider this.log("Disposed all disposables") ClineProvider.activeInstances.delete(this) + // Clean up any event listeners attached to this provider + this.removeAllListeners() + McpServerManager.unregisterProvider(this) } @@ -403,6 +422,7 @@ export class ClineProvider public static async isActiveTask(): Promise { const visibleProvider = await ClineProvider.getInstance() + if (!visibleProvider) { return false } @@ -653,7 +673,7 @@ export class ClineProvider rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, parentTask, taskNumber: this.clineStack.length + 1, - onCreated: (instance) => this.emit("taskCreated", instance), + onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance), ...options, }) @@ -732,7 +752,7 @@ export class ClineProvider rootTask: historyItem.rootTask, parentTask: historyItem.parentTask, taskNumber: historyItem.number, - onCreated: (instance) => this.emit("taskCreated", instance), + onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance), }) await this.addClineToStack(task) @@ -942,7 +962,7 @@ export class ClineProvider if (cline) { TelemetryService.instance.captureModeSwitch(cline.taskId, newMode) - cline.emit("taskModeSwitched", cline.taskId, newMode) + cline.emit(RooCodeEventName.TaskModeSwitched, cline.taskId, newMode) // Store the current mode in case we need to rollback const previousMode = (cline as any)._taskMode diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index d19ab1e6508..66c1db55a86 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -16,7 +16,6 @@ import { Task, TaskOptions } from "../../task/Task" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" -import { AsyncInvokeOutputDataConfig } from "@aws-sdk/client-bedrock-runtime" // Mock setup must come before imports vi.mock("../../prompts/sections/custom-instructions") @@ -215,6 +214,7 @@ vi.mock("../../task/Task", () => ({ setParentTask: vi.fn(), setRootTask: vi.fn(), taskId: taskId || "test-task-id", + emit: vi.fn(), }), ), })) diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index 6b19b47a38a..e7eff427cf7 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -900,12 +900,18 @@ describe("ClineProvider - Sticky Mode", () => { it("should handle errors during mode switch gracefully", async () => { await provider.resolveWebviewView(mockWebviewView) - // Create a mock task that throws on emit + // Create a mock task that throws on emit only for specific events + let emitCallCount = 0 const mockTask = { taskId: "test-task-id", _taskMode: "code", - emit: vi.fn().mockImplementation(() => { - throw new Error("Emit failed") + emit: vi.fn().mockImplementation((event) => { + emitCallCount++ + // Only throw on the second emit call (taskModeSwitched event) + // The first call is for TaskFocused in addClineToStack + if (emitCallCount === 2 && event === "taskModeSwitched") { + throw new Error("Emit failed") + } }), saveClineMessages: vi.fn(), clineMessages: [], @@ -915,13 +921,42 @@ describe("ClineProvider - Sticky Mode", () => { // Add task to provider stack await provider.addClineToStack(mockTask as any) + // Mock getGlobalState to return task history + vi.spyOn(provider as any, "getGlobalState").mockReturnValue([ + { + id: mockTask.taskId, + ts: Date.now(), + task: "Test task", + number: 1, + tokensIn: 0, + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + }, + ]) + + // Mock updateTaskHistory + vi.spyOn(provider, "updateTaskHistory").mockImplementation(() => Promise.resolve([])) + // Mock console.error to suppress error output const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + // Clear previous mock calls to isolate this test + vi.mocked(mockContext.globalState.update).mockClear() + // The handleModeSwitch method doesn't catch errors from emit, so it will throw - // This is the actual behavior based on the test failure + // The error is thrown before the task's mode is updated await expect(provider.handleModeSwitch("architect")).rejects.toThrow("Emit failed") + // Since the error is thrown before updating the task's _taskMode, + // neither the task mode nor global state are updated + const modeCalls = vi.mocked(mockContext.globalState.update).mock.calls.filter((call) => call[0] === "mode") + expect(modeCalls.length).toBe(0) + + // The task's mode should NOT have been updated since the error occurred first + expect(mockTask._taskMode).toBe("code") + consoleErrorSpy.mockRestore() }) diff --git a/src/extension/api.ts b/src/extension/api.ts index fba10d041a0..49710c32e40 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -5,22 +5,21 @@ import * as path from "path" import * as os from "os" import { - RooCodeAPI, - RooCodeSettings, - RooCodeEvents, + type RooCodeAPI, + type RooCodeSettings, + type RooCodeEvents, + type ProviderSettings, + type ProviderSettingsEntry, + type TaskEvent, RooCodeEventName, - ProviderSettings, - ProviderSettingsEntry, + TaskCommandName, isSecretStateKey, IpcOrigin, IpcMessageType, - TaskCommandName, - TaskEvent, } from "@roo-code/types" import { IpcServer } from "@roo-code/ipc" import { Package } from "../shared/package" -import { getWorkspacePath } from "../utils/path" import { ClineProvider } from "../core/webview/ClineProvider" import { openClineInNewTab } from "../activate/registerCommands" @@ -214,58 +213,86 @@ export class API extends EventEmitter implements RooCodeAPI { } private registerListeners(provider: ClineProvider) { - provider.on("taskCreated", (cline) => { - cline.on("taskStarted", async () => { - this.emit(RooCodeEventName.TaskStarted, cline.taskId) - this.taskMap.set(cline.taskId, provider) - await this.fileLog(`[${new Date().toISOString()}] taskStarted -> ${cline.taskId}\n`) + provider.on(RooCodeEventName.TaskCreated, (task) => { + // Task Lifecycle + + task.on(RooCodeEventName.TaskStarted, async () => { + this.emit(RooCodeEventName.TaskStarted, task.taskId) + this.taskMap.set(task.taskId, provider) + await this.fileLog(`[${new Date().toISOString()}] taskStarted -> ${task.taskId}\n`) }) - cline.on("message", async (message) => { - this.emit(RooCodeEventName.Message, { taskId: cline.taskId, ...message }) + task.on(RooCodeEventName.TaskCompleted, async (_, tokenUsage, toolUsage) => { + let isSubtask = false - if (message.message.partial !== true) { - await this.fileLog(`[${new Date().toISOString()}] ${JSON.stringify(message.message, null, 2)}\n`) + if (typeof task.rootTask !== "undefined") { + isSubtask = true } + + this.emit(RooCodeEventName.TaskCompleted, task.taskId, tokenUsage, toolUsage, { isSubtask: isSubtask }) + this.taskMap.delete(task.taskId) + + await this.fileLog( + `[${new Date().toISOString()}] taskCompleted -> ${task.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`, + ) }) - cline.on("taskModeSwitched", (taskId, mode) => this.emit(RooCodeEventName.TaskModeSwitched, taskId, mode)) + task.on(RooCodeEventName.TaskAborted, () => { + this.emit(RooCodeEventName.TaskAborted, task.taskId) + this.taskMap.delete(task.taskId) + }) - cline.on("taskAskResponded", () => this.emit(RooCodeEventName.TaskAskResponded, cline.taskId)) + // Optional: + // RooCodeEventName.TaskFocused + // RooCodeEventName.TaskUnfocused + // RooCodeEventName.TaskActive + // RooCodeEventName.TaskIdle - cline.on("taskAborted", () => { - this.emit(RooCodeEventName.TaskAborted, cline.taskId) - this.taskMap.delete(cline.taskId) + // Subtask Lifecycle + + task.on(RooCodeEventName.TaskPaused, () => { + this.emit(RooCodeEventName.TaskPaused, task.taskId) }) - cline.on("taskCompleted", async (_, tokenUsage, toolUsage) => { - let isSubtask = false + task.on(RooCodeEventName.TaskUnpaused, () => { + this.emit(RooCodeEventName.TaskUnpaused, task.taskId) + }) - if (cline.rootTask != undefined) { - isSubtask = true + task.on(RooCodeEventName.TaskSpawned, (childTaskId) => { + this.emit(RooCodeEventName.TaskSpawned, task.taskId, childTaskId) + }) + + // Task Execution + + task.on(RooCodeEventName.Message, async (message) => { + this.emit(RooCodeEventName.Message, { taskId: task.taskId, ...message }) + + if (message.message.partial !== true) { + await this.fileLog(`[${new Date().toISOString()}] ${JSON.stringify(message.message, null, 2)}\n`) } + }) - this.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage, { isSubtask: isSubtask }) - this.taskMap.delete(cline.taskId) + task.on(RooCodeEventName.TaskModeSwitched, (taskId, mode) => { + this.emit(RooCodeEventName.TaskModeSwitched, taskId, mode) + }) - await this.fileLog( - `[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`, - ) + task.on(RooCodeEventName.TaskAskResponded, () => { + this.emit(RooCodeEventName.TaskAskResponded, task.taskId) }) - cline.on("taskSpawned", (childTaskId) => this.emit(RooCodeEventName.TaskSpawned, cline.taskId, childTaskId)) - cline.on("taskPaused", () => this.emit(RooCodeEventName.TaskPaused, cline.taskId)) - cline.on("taskUnpaused", () => this.emit(RooCodeEventName.TaskUnpaused, cline.taskId)) + // Task Analytics - cline.on("taskTokenUsageUpdated", (_, usage) => - this.emit(RooCodeEventName.TaskTokenUsageUpdated, cline.taskId, usage), - ) + task.on(RooCodeEventName.TaskToolFailed, (taskId, tool, error) => { + this.emit(RooCodeEventName.TaskToolFailed, taskId, tool, error) + }) + + task.on(RooCodeEventName.TaskTokenUsageUpdated, (_, usage) => { + this.emit(RooCodeEventName.TaskTokenUsageUpdated, task.taskId, usage) + }) - cline.on("taskToolFailed", (taskId, tool, error) => - this.emit(RooCodeEventName.TaskToolFailed, taskId, tool, error), - ) + // Let's go! - this.emit(RooCodeEventName.TaskCreated, cline.taskId) + this.emit(RooCodeEventName.TaskCreated, task.taskId) }) } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 22de35accc6..0c6a84a65e9 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -251,7 +251,7 @@ function getSelectedModel({ case "cerebras": { const id = apiConfiguration.apiModelId ?? cerebrasDefaultModelId const info = cerebrasModels[id as keyof typeof cerebrasModels] - return { id, info } + return { id, info } } case "sambanova": { const id = apiConfiguration.apiModelId ?? sambaNovaDefaultModelId