Просмотр исходного кода

Task and TaskProvider event emitter cleanup + a few new events (#6606)

Co-authored-by: Roo Code <[email protected]>
Chris Estreich 5 месяцев назад
Родитель
Сommit
b2d2a2c5d2
46 измененных файлов с 1114 добавлено и 679 удалено
  1. 5 5
      apps/vscode-e2e/src/suite/markdown-lists.test.ts
  2. 3 1
      apps/vscode-e2e/src/suite/modes.test.ts
  3. 3 3
      apps/vscode-e2e/src/suite/subtasks.test.ts
  4. 2 2
      apps/vscode-e2e/src/suite/task.test.ts
  5. 31 31
      apps/vscode-e2e/src/suite/tools/apply-diff.test.ts
  6. 25 25
      apps/vscode-e2e/src/suite/tools/execute-command.test.ts
  7. 25 25
      apps/vscode-e2e/src/suite/tools/insert-content.test.ts
  8. 17 17
      apps/vscode-e2e/src/suite/tools/list-files.test.ts
  9. 31 31
      apps/vscode-e2e/src/suite/tools/read-file.test.ts
  10. 25 25
      apps/vscode-e2e/src/suite/tools/search-and-replace.test.ts
  11. 33 33
      apps/vscode-e2e/src/suite/tools/search-files.test.ts
  12. 27 27
      apps/vscode-e2e/src/suite/tools/use-mcp-tool.test.ts
  13. 13 13
      apps/vscode-e2e/src/suite/tools/write-to-file.test.ts
  14. 3 3
      apps/vscode-e2e/src/suite/utils.ts
  15. 122 0
      packages/cloud/src/CloudAPI.ts
  16. 9 4
      packages/cloud/src/CloudService.ts
  17. 1 1
      packages/cloud/src/CloudSettingsService.ts
  18. 43 0
      packages/cloud/src/CloudShareService.ts
  19. 0 88
      packages/cloud/src/ShareService.ts
  20. 1 1
      packages/cloud/src/StaticSettingsService.ts
  21. 1 1
      packages/cloud/src/TelemetryClient.ts
  22. 6 4
      packages/cloud/src/__tests__/CloudService.test.ts
  23. 3 3
      packages/cloud/src/__tests__/CloudSettingsService.test.ts
  24. 26 14
      packages/cloud/src/__tests__/CloudShareService.test.ts
  25. 15 15
      packages/cloud/src/__tests__/auth/WebAuthService.spec.ts
  26. 1 0
      packages/cloud/src/auth/AuthService.ts
  27. 3 0
      packages/cloud/src/auth/StaticTokenAuthService.ts
  28. 26 17
      packages/cloud/src/auth/WebAuthService.ts
  29. 0 2
      packages/cloud/src/config.ts
  30. 42 0
      packages/cloud/src/errors.ts
  31. 3 1
      packages/cloud/src/index.ts
  32. 3 18
      packages/types/src/api.ts
  33. 1 0
      packages/types/src/cloud.ts
  34. 192 0
      packages/types/src/events.ts
  35. 6 4
      packages/types/src/index.ts
  36. 21 141
      packages/types/src/ipc.ts
  37. 20 0
      packages/types/src/message.ts
  38. 98 0
      packages/types/src/task.ts
  39. 53 36
      src/core/task/Task.ts
  40. 6 3
      src/core/tools/attemptCompletionTool.ts
  41. 4 2
      src/core/tools/newTaskTool.ts
  42. 56 36
      src/core/webview/ClineProvider.ts
  43. 1 1
      src/core/webview/__tests__/ClineProvider.spec.ts
  44. 39 4
      src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  45. 68 41
      src/extension/api.ts
  46. 1 1
      webview-ui/src/components/ui/hooks/useSelectedModel.ts

+ 5 - 5
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)
 			}

+ 3 - 1
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 },

+ 3 - 3
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<string, ClineMessage[]> = {}
 
-		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()

+ 2 - 2
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)
 			}

+ 31 - 31
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)
 		}
 	})
 })

+ 25 - 25
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)
 		}
 	})
 })

+ 25 - 25
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

+ 17 - 17
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)
 		}
 	})
 })

+ 31 - 31
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)
 		}
 	})
 })

+ 25 - 25
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)
 		}
 	})
 })

+ 33 - 33
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)
 		}
 	})
 })

+ 27 - 27
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)
 		}
 	})
 })

+ 13 - 13
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)
 		}
 	})
 })

+ 3 - 3
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<string>()
-	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<string>()
-	api.on("taskCompleted", (taskId) => set.add(taskId))
+	api.on(RooCodeEventName.TaskCompleted, (taskId) => set.add(taskId))
 	await waitFor(() => set.has(taskId), options)
 }
 

+ 122 - 0
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<RequestInit, "headers"> {
+	timeout?: number
+	headers?: Record<string, string>
+}
+
+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<T>(
+		endpoint: string,
+		options: CloudAPIRequestOptions & {
+			parseResponse?: (data: unknown) => T
+		} = {},
+	): Promise<T> {
+		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<never> {
+		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<ShareResponse> {
+		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
+	}
+}

+ 9 - 4
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<CloudServiceEvents> 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<CloudServiceEvents> 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<CloudServiceEvents> 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<CloudServiceEvents> 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)

+ 1 - 1
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"

+ 43 - 0
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<ShareResponse> {
+		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<boolean> {
+		try {
+			return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing
+		} catch (error) {
+			this.log("[ShareService] Error checking if task can be shared:", error)
+			return false
+		}
+	}
+}

+ 0 - 88
packages/cloud/src/ShareService.ts

@@ -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<boolean> {
-		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
-		}
-	}
-}

+ 1 - 1
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.
 	}
 }

+ 1 - 1
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"
 

+ 6 - 4
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)

+ 3 - 3
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",
 				},

+ 26 - 14
packages/cloud/src/__tests__/ShareService.test.ts → 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),
+			)
 		})
 	})
 })

+ 15 - 15
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
 

+ 1 - 0
packages/cloud/src/auth/AuthService.ts

@@ -1,4 +1,5 @@
 import EventEmitter from "events"
+
 import type { CloudUserInfo } from "@roo-code/types"
 
 export interface AuthServiceEvents {

+ 3 - 0
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<AuthServiceEvents> implements AuthService {

+ 26 - 17
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<typeof authCredentialsSchema>
 
-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<AuthServiceEvents> implements AuthService {
 	private context: vscode.ExtensionContext
 	private timer: RefreshTimer
@@ -94,8 +98,9 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> 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<AuthServiceEvents> 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<AuthServiceEvents> 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<AuthServiceEvents> 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<AuthServiceEvents> implements A
 
 				if (primaryOrgMembership) {
 					this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
+
 					this.log("[auth] Legacy credentials: Found organization membership:", {
 						id: primaryOrgMembership.organization.id,
 						name: primaryOrgMembership.organization.name,

+ 0 - 2
packages/cloud/src/Config.ts → 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

+ 42 - 0
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)
+	}
+}

+ 3 - 1
packages/cloud/src/index.ts

@@ -1,2 +1,4 @@
+export * from "./config"
+
+export * from "./CloudAPI"
 export * from "./CloudService"
-export * from "./Config"

+ 3 - 18
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<RooCodeAPIEvents> {
 	/**

+ 1 - 0
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

+ 192 - 0
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<typeof rooCodeEventsSchema>
+
+/**
+ * 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<typeof taskEventSchema>

+ 6 - 4
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"

+ 21 - 141
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<typeof isSubtaskSchema>
+
+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<typeof rooCodeEventsSchema>
-
 /**
  * Ack
  */
@@ -69,7 +37,7 @@ export const ackSchema = z.object({
 export type Ack = z.infer<typeof ackSchema>
 
 /**
- * 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<typeof taskCommandSchema>
 
-/**
- * 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<typeof taskEventSchema>
-
 /**
  * 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<typeof ipcMessageSchema>
 
 /**
- * Client
+ * IpcClientEvents
  */
 
 export type IpcClientEvents = {
@@ -231,7 +111,7 @@ export type IpcClientEvents = {
 }
 
 /**
- * Server
+ * IpcServerEvents
  */
 
 export type IpcServerEvents = {

+ 20 - 0
packages/types/src/message.ts

@@ -44,6 +44,26 @@ export const clineAskSchema = z.enum(clineAsks)
 
 export type ClineAsk = z.infer<typeof clineAskSchema>
 
+/**
+ * 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
  */

+ 98 - 0
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<TaskLike>
+	cancelTask(): Promise<void>
+	clearTask(): Promise<void>
+	postStateToWebview(): Promise<void>
+
+	getState(): Promise<TaskProviderState>
+
+	postMessageToWebview(message: unknown): Promise<void>
+
+	on<K extends keyof TaskProviderEvents>(
+		event: K,
+		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
+	): this
+
+	off<K extends keyof TaskProviderEvents>(
+		event: K,
+		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
+	): 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<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
+	off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): 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]
+}

+ 53 - 36
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<void>
-}
-
 export type TaskOptions = {
 	provider: ClineProvider
 	apiConfiguration: ProviderSettings
@@ -132,7 +119,7 @@ export type TaskOptions = {
 	onCreated?: (task: Task) => void
 }
 
-export class Task extends EventEmitter<TaskEvents> {
+export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	todoList?: TodoItem[]
 	readonly taskId: string
 	readonly instanceId: string
@@ -189,6 +176,7 @@ export class Task extends EventEmitter<TaskEvents> {
 	providerRef: WeakRef<ClineProvider>
 	private readonly globalStoragePath: string
 	abort: boolean = false
+	blockingAsk?: BlockingAsk
 	didFinishAbortingStream = false
 	abandoned = false
 	isInitialized = false
@@ -545,7 +533,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		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<TaskEvents> {
 	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<TaskEvents> {
 				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<TaskEvents> {
 			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<TaskEvents> {
 		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<TaskEvents> {
 	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<TaskEvents> {
 			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<TaskEvents> {
 		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<TaskEvents> {
 		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<TaskEvents> {
 
 		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<TaskEvents> {
 	}
 
 	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<TaskEvents> {
 		}
 
 		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<TaskEvents> {
 		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<TaskEvents> {
 					// 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<TaskEvents> {
 		this.toolUsage[toolName].failures++
 
 		if (error) {
-			this.emit("taskToolFailed", this.taskId, toolName, error)
+			this.emit(RooCodeEventName.TaskToolFailed, this.taskId, toolName, error)
 		}
 	}
 

+ 6 - 3
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()

+ 4 - 2
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
 		}

+ 56 - 36
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<ClineProviderEvents>
-	implements vscode.WebviewViewProvider, TelemetryPropertiesProvider
+	extends EventEmitter<TaskProviderEvents>
+	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<boolean> {
 		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

+ 1 - 1
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(),
 			}),
 		),
 }))

+ 39 - 4
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()
 		})
 

+ 68 - 41
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<RooCodeEvents> 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)
 		})
 	}
 

+ 1 - 1
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