Browse Source

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

Co-authored-by: Roo Code <[email protected]>
Chris Estreich 5 months ago
parent
commit
b2d2a2c5d2
46 changed files with 1114 additions and 679 deletions
  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 * as assert from "assert"
 
 
-import type { ClineMessage } from "@roo-code/types"
+import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
 
 
 import { waitUntilCompleted } from "./utils"
 import { waitUntilCompleted } from "./utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
@@ -13,7 +13,7 @@ suite("Markdown List Rendering", function () {
 
 
 		const messages: ClineMessage[] = []
 		const messages: ClineMessage[] = []
 
 
-		api.on("message", ({ message }: { message: ClineMessage }) => {
+		api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages.push(message)
 				messages.push(message)
 			}
 			}
@@ -50,7 +50,7 @@ suite("Markdown List Rendering", function () {
 
 
 		const messages: ClineMessage[] = []
 		const messages: ClineMessage[] = []
 
 
-		api.on("message", ({ message }: { message: ClineMessage }) => {
+		api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages.push(message)
 				messages.push(message)
 			}
 			}
@@ -87,7 +87,7 @@ suite("Markdown List Rendering", function () {
 
 
 		const messages: ClineMessage[] = []
 		const messages: ClineMessage[] = []
 
 
-		api.on("message", ({ message }: { message: ClineMessage }) => {
+		api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages.push(message)
 				messages.push(message)
 			}
 			}
@@ -139,7 +139,7 @@ suite("Markdown List Rendering", function () {
 
 
 		const messages: ClineMessage[] = []
 		const messages: ClineMessage[] = []
 
 
-		api.on("message", ({ message }: { message: ClineMessage }) => {
+		api.on(RooCodeEventName.Message, ({ message }: { message: ClineMessage }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages.push(message)
 				messages.push(message)
 			}
 			}

+ 3 - 1
apps/vscode-e2e/src/suite/modes.test.ts

@@ -1,5 +1,7 @@
 import * as assert from "assert"
 import * as assert from "assert"
 
 
+import { RooCodeEventName } from "@roo-code/types"
+
 import { waitUntilCompleted } from "./utils"
 import { waitUntilCompleted } from "./utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
 
 
@@ -9,7 +11,7 @@ suite("Roo Code Modes", function () {
 	test("Should handle switching modes correctly", async () => {
 	test("Should handle switching modes correctly", async () => {
 		const modes: string[] = []
 		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({
 		const switchModesTaskId = await globalThis.api.startNewTask({
 			configuration: { mode: "code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true },
 			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 * 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"
 import { sleep, waitFor, waitUntilCompleted } from "./utils"
 
 
@@ -10,7 +10,7 @@ suite.skip("Roo Code Subtasks", () => {
 
 
 		const messages: Record<string, ClineMessage[]> = {}
 		const messages: Record<string, ClineMessage[]> = {}
 
 
-		api.on("message", ({ taskId, message }) => {
+		api.on(RooCodeEventName.Message, ({ taskId, message }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages[taskId] = messages[taskId] || []
 				messages[taskId] = messages[taskId] || []
 				messages[taskId].push(message)
 				messages[taskId].push(message)
@@ -37,7 +37,7 @@ suite.skip("Roo Code Subtasks", () => {
 		let spawnedTaskId: string | undefined = undefined
 		let spawnedTaskId: string | undefined = undefined
 
 
 		// Wait for the subtask to be spawned and then cancel it.
 		// 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 waitFor(() => !!spawnedTaskId)
 		await sleep(1_000) // Give the task a chance to start and populate the history.
 		await sleep(1_000) // Give the task a chance to start and populate the history.
 		await api.cancelCurrentTask()
 		await api.cancelCurrentTask()

+ 2 - 2
apps/vscode-e2e/src/suite/task.test.ts

@@ -1,6 +1,6 @@
 import * as assert from "assert"
 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 { waitUntilCompleted } from "./utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
 import { setDefaultSuiteTimeout } from "./test-utils"
@@ -13,7 +13,7 @@ suite("Roo Code Task", function () {
 
 
 		const messages: ClineMessage[] = []
 		const messages: ClineMessage[] = []
 
 
-		api.on("message", ({ message }) => {
+		api.on(RooCodeEventName.Message, ({ message }) => {
 			if (message.type === "say" && message.partial === false) {
 			if (message.type === "say" && message.partial === false) {
 				messages.push(message)
 				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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -201,7 +201,7 @@ function validateInput(input) {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -209,7 +209,7 @@ function validateInput(input) {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! apply_diff tool executed and file modified successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		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)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -322,7 +322,7 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`,
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! apply_diff tool executed and multiple replacements applied successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -432,14 +432,14 @@ function keepThis() {
 				taskStarted = true
 				taskStarted = true
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! apply_diff tool executed and targeted modification successful")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -540,14 +540,14 @@ ${testFile.content}\nAssume the file exists and you can modify it directly.`,
 				taskStarted = true
 				taskStarted = true
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! apply_diff attempted and error handled gracefully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -672,7 +672,7 @@ function checkInput(input) {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -680,7 +680,7 @@ function checkInput(input) {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! apply_diff tool executed and multiple search/replace blocks applied successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep, waitUntilCompleted } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -154,7 +154,7 @@ suite("Roo Code execute_command Tool", function () {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -162,7 +162,7 @@ suite("Roo Code execute_command Tool", function () {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Command executed successfully")
 		} finally {
 		} finally {
 			// Clean up event listeners
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		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)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			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)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Command executed in custom directory")
 		} finally {
 		} finally {
 			// Clean up event listeners
 			// 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
 			// Clean up subdirectory
 			try {
 			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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		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)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			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)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Multiple commands executed successfully")
 		} finally {
 		} finally {
 			// Clean up event listeners
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		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)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			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)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Long-running command handled successfully")
 		} finally {
 		} finally {
 			// Clean up event listeners
 			// 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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 			// Listen for task events
 			const taskStartedHandler = (id: string) => {
 			const taskStartedHandler = (id: string) => {
@@ -154,7 +154,7 @@ ${testFile.content}`
 					console.log("Task started:", id)
 					console.log("Task started:", id)
 				}
 				}
 			}
 			}
-			api.on("taskStarted", taskStartedHandler)
+			api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 			const taskCompletedHandler = (id: string) => {
 			const taskCompletedHandler = (id: string) => {
 				if (id === taskId) {
 				if (id === taskId) {
@@ -162,7 +162,7 @@ ${testFile.content}`
 					console.log("Task completed:", id)
 					console.log("Task completed:", id)
 				}
 				}
 			}
 			}
-			api.on("taskCompleted", taskCompletedHandler)
+			api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 			let taskId: string
 			let taskId: string
 			try {
 			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")
 				console.log("Test passed! insert_content tool executed and content inserted at beginning successfully")
 			} finally {
 			} 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 {
 		try {
@@ -286,7 +286,7 @@ ${insertContent}`
 				}
 				}
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task events
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -295,7 +295,7 @@ ${insertContent}`
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -303,7 +303,7 @@ ${insertContent}`
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -388,7 +388,7 @@ ${testFile.content}`
 						}
 						}
 					}
 					}
 				}
 				}
-				api.on("message", messageHandler)
+				api.on(RooCodeEventName.Message, messageHandler)
 
 
 				// Listen for task events
 				// Listen for task events
 				const taskStartedHandler = (id: string) => {
 				const taskStartedHandler = (id: string) => {
@@ -397,7 +397,7 @@ ${testFile.content}`
 						console.log("Task started:", id)
 						console.log("Task started:", id)
 					}
 					}
 				}
 				}
-				api.on("taskStarted", taskStartedHandler)
+				api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 				const taskCompletedHandler = (id: string) => {
 				const taskCompletedHandler = (id: string) => {
 					if (id === taskId) {
 					if (id === taskId) {
@@ -405,7 +405,7 @@ ${testFile.content}`
 						console.log("Task completed:", id)
 						console.log("Task completed:", id)
 					}
 					}
 				}
 				}
-				api.on("taskCompleted", taskCompletedHandler)
+				api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 				let taskId: string
 				let taskId: string
 				try {
 				try {
@@ -490,7 +490,7 @@ And this is the second line`
 								}
 								}
 							}
 							}
 						}
 						}
-						api.on("message", messageHandler)
+						api.on(RooCodeEventName.Message, messageHandler)
 
 
 						// Listen for task events
 						// Listen for task events
 						const taskStartedHandler = (id: string) => {
 						const taskStartedHandler = (id: string) => {
@@ -499,7 +499,7 @@ And this is the second line`
 								console.log("Task started:", id)
 								console.log("Task started:", id)
 							}
 							}
 						}
 						}
-						api.on("taskStarted", taskStartedHandler)
+						api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 						const taskCompletedHandler = (id: string) => {
 						const taskCompletedHandler = (id: string) => {
 							if (id === taskId) {
 							if (id === taskId) {
@@ -507,7 +507,7 @@ And this is the second line`
 								console.log("Task completed:", id)
 								console.log("Task completed:", id)
 							}
 							}
 						}
 						}
-						api.on("taskCompleted", taskCompletedHandler)
+						api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 						let taskId: string
 						let taskId: string
 						try {
 						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",
 								"Test passed! insert_content tool executed and content inserted into empty file successfully",
 							)
 							)
 						} finally {
 						} 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
 					// 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")
 					console.log("Test passed! insert_content tool executed and multiline content inserted successfully")
 				} finally {
 				} 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")
 			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")
 			console.log("Test passed! insert_content tool executed and content inserted at end successfully")
 		} finally {
 		} 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
 	// 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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -215,7 +215,7 @@ This directory contains various files and subdirectories for testing the list_fi
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Directory listing (non-recursive) executed successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -318,7 +318,7 @@ This directory contains various files and subdirectories for testing the list_fi
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Directory listing (recursive) executed successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -428,7 +428,7 @@ This directory contains various files and subdirectories for testing the list_fi
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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 })
 			await fs.rm(testDir, { recursive: true, force: true })
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -531,7 +531,7 @@ This directory contains various files and subdirectories for testing the list_fi
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Workspace root directory listing executed successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 os from "os"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task events
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -189,7 +189,7 @@ suite("Roo Code read_file Tool", function () {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -197,7 +197,7 @@ suite("Roo Code read_file Tool", function () {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -259,9 +259,9 @@ suite("Roo Code read_file Tool", function () {
 			console.log("Test passed! File read successfully with correct content")
 			console.log("Test passed! File read successfully with correct content")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -322,7 +322,7 @@ suite("Roo Code read_file Tool", function () {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -371,8 +371,8 @@ suite("Roo Code read_file Tool", function () {
 			console.log("Test passed! Multiline file read successfully with correct content")
 			console.log("Test passed! Multiline file read successfully with correct content")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -433,7 +433,7 @@ suite("Roo Code read_file Tool", function () {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -484,8 +484,8 @@ suite("Roo Code read_file Tool", function () {
 			console.log("Test passed! File read with line range successfully")
 			console.log("Test passed! File read with line range successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -520,7 +520,7 @@ suite("Roo Code read_file Tool", function () {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -556,8 +556,8 @@ suite("Roo Code read_file Tool", function () {
 			console.log("Test passed! Non-existent file handled correctly")
 			console.log("Test passed! Non-existent file handled correctly")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -593,7 +593,7 @@ suite("Roo Code read_file Tool", function () {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -627,8 +627,8 @@ suite("Roo Code read_file Tool", function () {
 			console.log("Test passed! XML file read successfully")
 			console.log("Test passed! XML file read successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -659,7 +659,7 @@ suite("Roo Code read_file Tool", function () {
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Multiple files read successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		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
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Large file read efficiently")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -184,7 +184,7 @@ Final content`,
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -192,7 +192,7 @@ Final content`,
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! search_and_replace tool executed and file modified successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -312,7 +312,7 @@ function anotherNewFunction() {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -320,7 +320,7 @@ function anotherNewFunction() {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! search_and_replace tool executed with regex successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -438,7 +438,7 @@ Final content`
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -446,7 +446,7 @@ Final content`
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! search_and_replace tool executed and replaced multiple matches successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -558,7 +558,7 @@ Assume the file exists and you can modify it directly.`,
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -566,7 +566,7 @@ Assume the file exists and you can modify it directly.`,
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! search_and_replace tool executed and handled no matches correctly")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 path from "path"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -331,7 +331,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Function definitions found successfully with validated results")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -429,7 +429,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! TODO comments found successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -496,7 +496,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! TypeScript interfaces found with file pattern filter")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -562,7 +562,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! JSON configuration keys found successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -630,7 +630,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Nested directory search completed successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -698,7 +698,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Complex regex pattern search completed successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("AI completion message:", message.text?.substring(0, 300))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -783,7 +783,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! No-match scenario handled correctly")
 		} finally {
 		} finally {
 			// Clean up
 			// 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
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -891,7 +891,7 @@ The search should find matches across different file types and provide context f
 				taskCompleted = true
 				taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! Class definitions and async methods found successfully")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 os from "os"
 import * as vscode from "vscode"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-utils"
 import { setDefaultSuiteTimeout } from "../test-utils"
@@ -167,7 +167,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				console.error("Error:", message.text)
 				console.error("Error:", message.text)
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task events
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -176,7 +176,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -184,7 +184,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 		await sleep(2000) // Wait for Roo Code to fully initialize
 		await sleep(2000) // Wait for Roo Code to fully initialize
 
 
 		// Trigger MCP server detection by opening and modifying the file
 		// 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")
 			console.log("Test passed! MCP read_file tool used successfully and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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)
 				console.error("Error:", message.text)
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -352,7 +352,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				_taskCompleted = true
 				_taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! MCP write_file tool used successfully and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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)
 				console.error("Error:", message.text)
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -480,7 +480,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				_taskCompleted = true
 				_taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! MCP list_directory tool used successfully and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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)
 				console.error("Error:", message.text)
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -619,7 +619,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				_taskCompleted = true
 				_taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! MCP directory_tree tool used successfully and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("Attempt completion called:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -738,7 +738,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				_taskCompleted = true
 				_taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! MCP error handling verified and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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)
 				console.error("Error:", message.text)
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task completion
 		// Listen for task completion
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
@@ -840,7 +840,7 @@ suite.skip("Roo Code use_mcp_tool Tool", function () {
 				_taskCompleted = true
 				_taskCompleted = true
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		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")
 			console.log("Test passed! MCP message format validation successful and task completed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 path from "path"
 import * as os from "os"
 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 { waitFor, sleep } from "../utils"
 import { setDefaultSuiteTimeout } from "../test-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))
 				console.log("AI response:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task events
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -119,7 +119,7 @@ suite("Roo Code write_to_file Tool", function () {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -127,7 +127,7 @@ suite("Roo Code write_to_file Tool", function () {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -259,9 +259,9 @@ suite("Roo Code write_to_file Tool", function () {
 			console.log("write_to_file tool was properly executed")
 			console.log("write_to_file tool was properly executed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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))
 				console.log("Tool request:", message.text?.substring(0, 200))
 			}
 			}
 		}
 		}
-		api.on("message", messageHandler)
+		api.on(RooCodeEventName.Message, messageHandler)
 
 
 		// Listen for task events
 		// Listen for task events
 		const taskStartedHandler = (id: string) => {
 		const taskStartedHandler = (id: string) => {
@@ -311,7 +311,7 @@ suite("Roo Code write_to_file Tool", function () {
 				console.log("Task started:", id)
 				console.log("Task started:", id)
 			}
 			}
 		}
 		}
-		api.on("taskStarted", taskStartedHandler)
+		api.on(RooCodeEventName.TaskStarted, taskStartedHandler)
 
 
 		const taskCompletedHandler = (id: string) => {
 		const taskCompletedHandler = (id: string) => {
 			if (id === taskId) {
 			if (id === taskId) {
@@ -319,7 +319,7 @@ suite("Roo Code write_to_file Tool", function () {
 				console.log("Task completed:", id)
 				console.log("Task completed:", id)
 			}
 			}
 		}
 		}
-		api.on("taskCompleted", taskCompletedHandler)
+		api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
 
 
 		let taskId: string
 		let taskId: string
 		try {
 		try {
@@ -440,9 +440,9 @@ suite("Roo Code write_to_file Tool", function () {
 			console.log("write_to_file tool was properly executed")
 			console.log("write_to_file tool was properly executed")
 		} finally {
 		} finally {
 			// Clean up
 			// 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 = {
 type WaitForOptions = {
 	timeout?: number
 	timeout?: number
@@ -46,7 +46,7 @@ type WaitUntilAbortedOptions = WaitForOptions & {
 
 
 export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbortedOptions) => {
 export const waitUntilAborted = async ({ api, taskId, ...options }: WaitUntilAbortedOptions) => {
 	const set = new Set<string>()
 	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)
 	await waitFor(() => set.has(taskId), options)
 }
 }
 
 
@@ -57,7 +57,7 @@ type WaitUntilCompletedOptions = WaitForOptions & {
 
 
 export const waitUntilCompleted = async ({ api, taskId, ...options }: WaitUntilCompletedOptions) => {
 export const waitUntilCompleted = async ({ api, taskId, ...options }: WaitUntilCompletedOptions) => {
 	const set = new Set<string>()
 	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)
 	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 { TelemetryService } from "@roo-code/telemetry"
 
 
 import { CloudServiceEvents } from "./types"
 import { CloudServiceEvents } from "./types"
+import { TaskNotFoundError } from "./errors"
 import type { AuthService } from "./auth"
 import type { AuthService } from "./auth"
 import { WebAuthService, StaticTokenAuthService } from "./auth"
 import { WebAuthService, StaticTokenAuthService } from "./auth"
 import type { SettingsService } from "./SettingsService"
 import type { SettingsService } from "./SettingsService"
 import { CloudSettingsService } from "./CloudSettingsService"
 import { CloudSettingsService } from "./CloudSettingsService"
 import { StaticSettingsService } from "./StaticSettingsService"
 import { StaticSettingsService } from "./StaticSettingsService"
 import { TelemetryClient } from "./TelemetryClient"
 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 AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0]
 type AuthUserInfoPayload = CloudServiceEvents["user-info"][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 settingsListener: (data: SettingsPayload) => void
 	private settingsService: SettingsService | null = null
 	private settingsService: SettingsService | null = null
 	private telemetryClient: TelemetryClient | 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 isInitialized = false
 	private log: (...args: unknown[]) => void
 	private log: (...args: unknown[]) => void
 
 
@@ -87,8 +90,9 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
 				this.settingsService = cloudSettingsService
 				this.settingsService = cloudSettingsService
 			}
 			}
 
 
+			this.cloudAPI = new CloudAPI(this.authService, this.log)
 			this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
 			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 {
 			try {
 				TelemetryService.instance.register(this.telemetryClient)
 				TelemetryService.instance.register(this.telemetryClient)
@@ -209,7 +213,7 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
 			return await this.shareService!.shareTask(taskId, visibility)
 			return await this.shareService!.shareTask(taskId, visibility)
 		} catch (error) {
 		} catch (error) {
 			if (error instanceof TaskNotFoundError && clineMessages) {
 			if (error instanceof TaskNotFoundError && clineMessages) {
-				// Backfill messages and retry
+				// Backfill messages and retry.
 				await this.telemetryClient!.backfillMessages(clineMessages, taskId)
 				await this.telemetryClient!.backfillMessages(clineMessages, taskId)
 				return await this.shareService!.shareTask(taskId, visibility)
 				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("auth-state-changed", this.authStateListener)
 			this.authService.off("user-info", this.authUserInfoListener)
 			this.authService.off("user-info", this.authUserInfoListener)
 		}
 		}
+
 		if (this.settingsService) {
 		if (this.settingsService) {
 			if (this.settingsService instanceof CloudSettingsService) {
 			if (this.settingsService instanceof CloudSettingsService) {
 				this.settingsService.off("settings-updated", this.settingsListener)
 				this.settingsService.off("settings-updated", this.settingsListener)

+ 1 - 1
packages/cloud/src/CloudSettingsService.ts

@@ -8,7 +8,7 @@ import {
 	organizationSettingsSchema,
 	organizationSettingsSchema,
 } from "@roo-code/types"
 } from "@roo-code/types"
 
 
-import { getRooCodeApiUrl } from "./Config"
+import { getRooCodeApiUrl } from "./config"
 import type { AuthService, AuthState } from "./auth"
 import type { AuthService, AuthState } from "./auth"
 import { RefreshTimer } from "./RefreshTimer"
 import { RefreshTimer } from "./RefreshTimer"
 import type { SettingsService } from "./SettingsService"
 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 {
 	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"
 } from "@roo-code/types"
 import { BaseTelemetryClient } from "@roo-code/telemetry"
 import { BaseTelemetryClient } from "@roo-code/telemetry"
 
 
-import { getRooCodeApiUrl } from "./Config"
+import { getRooCodeApiUrl } from "./config"
 import type { AuthService } from "./auth"
 import type { AuthService } from "./auth"
 import type { SettingsService } from "./SettingsService"
 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
 // npx vitest run src/__tests__/CloudService.test.ts
 
 
 import * as vscode from "vscode"
 import * as vscode from "vscode"
+
 import type { ClineMessage } from "@roo-code/types"
 import type { ClineMessage } from "@roo-code/types"
+import { TelemetryService } from "@roo-code/telemetry"
 
 
 import { CloudService } from "../CloudService"
 import { CloudService } from "../CloudService"
 import { WebAuthService } from "../auth/WebAuthService"
 import { WebAuthService } from "../auth/WebAuthService"
 import { CloudSettingsService } from "../CloudSettingsService"
 import { CloudSettingsService } from "../CloudSettingsService"
-import { ShareService, TaskNotFoundError } from "../ShareService"
+import { CloudShareService } from "../CloudShareService"
 import { TelemetryClient } from "../TelemetryClient"
 import { TelemetryClient } from "../TelemetryClient"
-import { TelemetryService } from "@roo-code/telemetry"
+import { TaskNotFoundError } from "../errors"
 
 
 vi.mock("vscode", () => ({
 vi.mock("vscode", () => ({
 	ExtensionContext: vi.fn(),
 	ExtensionContext: vi.fn(),
@@ -30,7 +32,7 @@ vi.mock("../auth/WebAuthService")
 
 
 vi.mock("../CloudSettingsService")
 vi.mock("../CloudSettingsService")
 
 
-vi.mock("../ShareService")
+vi.mock("../CloudShareService")
 
 
 vi.mock("../TelemetryClient")
 vi.mock("../TelemetryClient")
 
 
@@ -154,7 +156,7 @@ describe("CloudService", () => {
 
 
 		vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
 		vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
 		vi.mocked(CloudSettingsService).mockImplementation(() => mockSettingsService as unknown as CloudSettingsService)
 		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(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
 
 
 		vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
 		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
 // Mock dependencies
 vi.mock("../RefreshTimer")
 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
 // Mock fetch globally
@@ -338,7 +338,7 @@ describe("CloudSettingsService", () => {
 			const result = await timerCallback()
 			const result = await timerCallback()
 
 
 			expect(result).toBe(true)
 			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: {
 				headers: {
 					Authorization: "Bearer valid-token",
 					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 type { MockedFunction } from "vitest"
 import * as vscode from "vscode"
 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 { SettingsService } from "../SettingsService"
+import type { AuthService } from "../auth"
+import { CloudAPIError, TaskNotFoundError } from "../errors"
 
 
 // Mock fetch
 // Mock fetch
 const mockFetch = vi.fn()
 const mockFetch = vi.fn()
@@ -44,10 +46,11 @@ vi.mock("../utils", () => ({
 	getUserAgent: () => "Roo-Code 1.0.0",
 	getUserAgent: () => "Roo-Code 1.0.0",
 }))
 }))
 
 
-describe("ShareService", () => {
-	let shareService: ShareService
+describe("CloudShareService", () => {
+	let shareService: CloudShareService
 	let mockAuthService: AuthService
 	let mockAuthService: AuthService
 	let mockSettingsService: SettingsService
 	let mockSettingsService: SettingsService
+	let mockCloudAPI: CloudAPI
 	let mockLog: MockedFunction<(...args: unknown[]) => void>
 	let mockLog: MockedFunction<(...args: unknown[]) => void>
 
 
 	beforeEach(() => {
 	beforeEach(() => {
@@ -65,7 +68,8 @@ describe("ShareService", () => {
 			getSettings: vi.fn(),
 			getSettings: vi.fn(),
 		} as any
 		} as any
 
 
-		shareService = new ShareService(mockAuthService, mockSettingsService, mockLog)
+		mockCloudAPI = new CloudAPI(mockAuthService, mockLog)
+		shareService = new CloudShareService(mockCloudAPI, mockSettingsService, mockLog)
 	})
 	})
 
 
 	describe("shareTask", () => {
 	describe("shareTask", () => {
@@ -189,12 +193,12 @@ describe("ShareService", () => {
 				ok: false,
 				ok: false,
 				status: 404,
 				status: 404,
 				statusText: "Not Found",
 				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(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 () => {
 		it("should throw generic Error for non-404 HTTP errors", async () => {
@@ -203,12 +207,14 @@ describe("ShareService", () => {
 				ok: false,
 				ok: false,
 				status: 500,
 				status: 500,
 				statusText: "Internal Server Error",
 				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(
 			await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(
 				"HTTP 500: Internal Server Error",
 				"HTTP 500: Internal Server Error",
 			)
 			)
-			await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError)
 		})
 		})
 
 
 		it("should create TaskNotFoundError with correct properties", async () => {
 		it("should create TaskNotFoundError with correct properties", async () => {
@@ -217,6 +223,8 @@ describe("ShareService", () => {
 				ok: false,
 				ok: false,
 				status: 404,
 				status: 404,
 				statusText: "Not Found",
 				statusText: "Not Found",
+				json: vi.fn().mockRejectedValue(new Error("Invalid JSON")),
+				text: vi.fn().mockResolvedValue("Not Found"),
 			})
 			})
 
 
 			try {
 			try {
@@ -225,7 +233,7 @@ describe("ShareService", () => {
 			} catch (error) {
 			} catch (error) {
 				expect(error).toBeInstanceOf(TaskNotFoundError)
 				expect(error).toBeInstanceOf(TaskNotFoundError)
 				expect(error).toBeInstanceOf(Error)
 				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)
 			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()
 			const result = await shareService.canShareTask()
 
 
@@ -286,13 +294,17 @@ describe("ShareService", () => {
 		})
 		})
 
 
 		it("should handle errors gracefully", async () => {
 		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()
 			const result = await shareService.canShareTask()
 
 
 			expect(result).toBe(false)
 			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 crypto from "crypto"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 
 
 import { WebAuthService } from "../../auth/WebAuthService"
 import { WebAuthService } from "../../auth/WebAuthService"
 import { RefreshTimer } from "../../RefreshTimer"
 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
 // Mock external dependencies
 vi.mock("../../RefreshTimer")
 vi.mock("../../RefreshTimer")
-vi.mock("../../Config")
+vi.mock("../../config")
 vi.mock("../../utils")
 vi.mock("../../utils")
 vi.mock("crypto")
 vi.mock("crypto")
 
 
@@ -101,11 +101,11 @@ describe("WebAuthService", () => {
 		MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer)
 		MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer)
 
 
 		// Setup config mocks - use production URL by default to maintain existing test behavior
 		// 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
 		// 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
 		// Setup crypto mock
 		vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never)
 		vi.mocked(crypto.randomBytes).mockReturnValue(Buffer.from("test-random-bytes") as never)
@@ -977,7 +977,7 @@ describe("WebAuthService", () => {
 	describe("auth credentials key scoping", () => {
 	describe("auth credentials key scoping", () => {
 		it("should use default key when getClerkBaseUrl returns production URL", async () => {
 		it("should use default key when getClerkBaseUrl returns production URL", async () => {
 			// Mock getClerkBaseUrl to return production URL
 			// 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 service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			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 () => {
 		it("should use scoped key when getClerkBaseUrl returns custom URL", async () => {
 			const customUrl = "https://custom.clerk.com"
 			const customUrl = "https://custom.clerk.com"
 			// Mock getClerkBaseUrl to return custom URL
 			// 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 service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
@@ -1010,7 +1010,7 @@ describe("WebAuthService", () => {
 
 
 		it("should load credentials using scoped key", async () => {
 		it("should load credentials using scoped key", async () => {
 			const customUrl = "https://custom.clerk.com"
 			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 service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
@@ -1025,7 +1025,7 @@ describe("WebAuthService", () => {
 
 
 		it("should clear credentials using scoped key", async () => {
 		it("should clear credentials using scoped key", async () => {
 			const customUrl = "https://custom.clerk.com"
 			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 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 () => {
 		it("should listen for changes on scoped key", async () => {
 			const customUrl = "https://custom.clerk.com"
 			const customUrl = "https://custom.clerk.com"
-			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
 
 
 			let onDidChangeCallback: (e: { key: string }) => void
 			let onDidChangeCallback: (e: { key: string }) => void
 
 
@@ -1064,7 +1064,7 @@ describe("WebAuthService", () => {
 
 
 		it("should not respond to changes on different scoped keys", async () => {
 		it("should not respond to changes on different scoped keys", async () => {
 			const customUrl = "https://custom.clerk.com"
 			const customUrl = "https://custom.clerk.com"
-			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
 
 
 			let onDidChangeCallback: (e: { key: string }) => void
 			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 () => {
 		it("should not respond to changes on default key when using scoped key", async () => {
 			const customUrl = "https://custom.clerk.com"
 			const customUrl = "https://custom.clerk.com"
-			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
+			vi.mocked(getClerkBaseUrl).mockReturnValue(customUrl)
 
 
 			let onDidChangeCallback: (e: { key: string }) => void
 			let onDidChangeCallback: (e: { key: string }) => void
 
 

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

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

+ 3 - 0
packages/cloud/src/auth/StaticTokenAuthService.ts

@@ -1,6 +1,9 @@
 import EventEmitter from "events"
 import EventEmitter from "events"
+
 import * as vscode from "vscode"
 import * as vscode from "vscode"
+
 import type { CloudUserInfo } from "@roo-code/types"
 import type { CloudUserInfo } from "@roo-code/types"
+
 import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
 import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
 
 
 export class StaticTokenAuthService extends EventEmitter<AuthServiceEvents> implements 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 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 { getUserAgent } from "../utils"
+import { InvalidClientTokenError } from "../errors"
+import { RefreshTimer } from "../RefreshTimer"
+
 import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
 import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
 
 
+const AUTH_STATE_KEY = "clerk-auth-state"
+
+/**
+ * AuthCredentials
+ */
+
 const authCredentialsSchema = z.object({
 const authCredentialsSchema = z.object({
 	clientToken: z.string().min(1, "Client token cannot be empty"),
 	clientToken: z.string().min(1, "Client token cannot be empty"),
 	sessionId: z.string().min(1, "Session ID 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>
 type AuthCredentials = z.infer<typeof authCredentialsSchema>
 
 
-const AUTH_STATE_KEY = "clerk-auth-state"
+/**
+ * Clerk Schemas
+ */
 
 
 const clerkSignInResponseSchema = z.object({
 const clerkSignInResponseSchema = z.object({
 	response: z.object({
 	response: z.object({
@@ -33,8 +43,9 @@ const clerkCreateSessionTokenResponseSchema = z.object({
 
 
 const clerkMeResponseSchema = z.object({
 const clerkMeResponseSchema = z.object({
 	response: 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(),
 		image_url: z.string().optional(),
 		primary_email_address_id: z.string().optional(),
 		primary_email_address_id: z.string().optional(),
 		email_addresses: z
 		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 {
 export class WebAuthService extends EventEmitter<AuthServiceEvents> implements AuthService {
 	private context: vscode.ExtensionContext
 	private context: vscode.ExtensionContext
 	private timer: RefreshTimer
 	private timer: RefreshTimer
@@ -94,8 +98,9 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 		this.context = context
 		this.context = context
 		this.log = log || console.log
 		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()
 		const clerkBaseUrl = getClerkBaseUrl()
+
 		if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) {
 		if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) {
 			this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}`
 			this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}`
 		} else {
 		} else {
@@ -514,9 +519,13 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 			throw new Error(`HTTP ${response.status}: ${response.statusText}`)
 			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)
 		const names = [userData.first_name, userData.last_name].filter((name) => !!name)
 		userInfo.name = names.length > 0 ? names.join(" ") : undefined
 		userInfo.name = names.length > 0 ? names.join(" ") : undefined
@@ -529,8 +538,6 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 			)?.email_address
 			)?.email_address
 		}
 		}
 
 
-		userInfo.picture = userData.image_url
-
 		// Fetch organization info if user is in organization context
 		// Fetch organization info if user is in organization context
 		try {
 		try {
 			const storedOrgId = this.getStoredOrganizationId()
 			const storedOrgId = this.getStoredOrganizationId()
@@ -544,6 +551,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 
 
 					if (userMembership) {
 					if (userMembership) {
 						this.setUserOrganizationInfo(userInfo, userMembership)
 						this.setUserOrganizationInfo(userInfo, userMembership)
+
 						this.log("[auth] User in organization context:", {
 						this.log("[auth] User in organization context:", {
 							id: userMembership.organization.id,
 							id: userMembership.organization.id,
 							name: userMembership.organization.name,
 							name: userMembership.organization.name,
@@ -562,6 +570,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
 
 
 				if (primaryOrgMembership) {
 				if (primaryOrgMembership) {
 					this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
 					this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
+
 					this.log("[auth] Legacy credentials: Found organization membership:", {
 					this.log("[auth] Legacy credentials: Found organization membership:", {
 						id: primaryOrgMembership.organization.id,
 						id: primaryOrgMembership.organization.id,
 						name: primaryOrgMembership.organization.name,
 						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_CLERK_BASE_URL = "https://clerk.roocode.com"
 export const PRODUCTION_ROO_CODE_API_URL = "https://app.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 getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL
 export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_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 "./CloudService"
-export * from "./Config"

+ 3 - 18
packages/types/src/api.ts

@@ -1,27 +1,12 @@
 import type { EventEmitter } from "events"
 import type { EventEmitter } from "events"
 import type { Socket } from "net"
 import type { Socket } from "net"
 
 
+import type { RooCodeEvents } from "./events.js"
 import type { RooCodeSettings } from "./global-settings.js"
 import type { RooCodeSettings } from "./global-settings.js"
 import type { ProviderSettingsEntry, ProviderSettings } from "./provider-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> {
 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 {
 export interface CloudUserInfo {
+	id?: string
 	name?: string
 	name?: string
 	email?: string
 	email?: string
 	picture?: 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 "./api.js"
-export * from "./codebase-index.js"
 export * from "./cloud.js"
 export * from "./cloud.js"
+export * from "./codebase-index.js"
+export * from "./events.js"
 export * from "./experiment.js"
 export * from "./experiment.js"
 export * from "./followup.js"
 export * from "./followup.js"
 export * from "./global-settings.js"
 export * from "./global-settings.js"
@@ -15,9 +14,12 @@ export * from "./mode.js"
 export * from "./model.js"
 export * from "./model.js"
 export * from "./provider-settings.js"
 export * from "./provider-settings.js"
 export * from "./sharing.js"
 export * from "./sharing.js"
+export * from "./task.js"
+export * from "./todo.js"
 export * from "./telemetry.js"
 export * from "./telemetry.js"
 export * from "./terminal.js"
 export * from "./terminal.js"
 export * from "./tool.js"
 export * from "./tool.js"
 export * from "./type-fu.js"
 export * from "./type-fu.js"
 export * from "./vscode.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 { 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"
 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
  * Ack
  */
  */
@@ -69,7 +37,7 @@ export const ackSchema = z.object({
 export type Ack = z.infer<typeof ackSchema>
 export type Ack = z.infer<typeof ackSchema>
 
 
 /**
 /**
- * TaskCommand
+ * TaskCommandName
  */
  */
 
 
 export enum TaskCommandName {
 export enum TaskCommandName {
@@ -78,6 +46,10 @@ export enum TaskCommandName {
 	CloseTask = "CloseTask",
 	CloseTask = "CloseTask",
 }
 }
 
 
+/**
+ * TaskCommand
+ */
+
 export const taskCommandSchema = z.discriminatedUnion("commandName", [
 export const taskCommandSchema = z.discriminatedUnion("commandName", [
 	z.object({
 	z.object({
 		commandName: z.literal(TaskCommandName.StartNewTask),
 		commandName: z.literal(TaskCommandName.StartNewTask),
@@ -100,102 +72,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
 
 
 export type TaskCommand = z.infer<typeof taskCommandSchema>
 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
  * 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", [
 export const ipcMessageSchema = z.discriminatedUnion("type", [
 	z.object({
 	z.object({
 		type: z.literal(IpcMessageType.Ack),
 		type: z.literal(IpcMessageType.Ack),
@@ -219,7 +99,7 @@ export const ipcMessageSchema = z.discriminatedUnion("type", [
 export type IpcMessage = z.infer<typeof ipcMessageSchema>
 export type IpcMessage = z.infer<typeof ipcMessageSchema>
 
 
 /**
 /**
- * Client
+ * IpcClientEvents
  */
  */
 
 
 export type IpcClientEvents = {
 export type IpcClientEvents = {
@@ -231,7 +111,7 @@ export type IpcClientEvents = {
 }
 }
 
 
 /**
 /**
- * Server
+ * IpcServerEvents
  */
  */
 
 
 export type 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>
 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
  * 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 { serializeError } from "serialize-error"
 
 
 import {
 import {
+	type TaskLike,
+	type TaskEvents,
 	type ProviderSettings,
 	type ProviderSettings,
 	type TokenUsage,
 	type TokenUsage,
 	type ToolUsage,
 	type ToolUsage,
 	type ToolName,
 	type ToolName,
 	type ContextCondense,
 	type ContextCondense,
-	type ClineAsk,
 	type ClineMessage,
 	type ClineMessage,
 	type ClineSay,
 	type ClineSay,
+	type ClineAsk,
+	type BlockingAsk,
 	type ToolProgressStatus,
 	type ToolProgressStatus,
-	DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
 	type HistoryItem,
 	type HistoryItem,
+	RooCodeEventName,
 	TelemetryEventName,
 	TelemetryEventName,
 	TodoItem,
 	TodoItem,
 	getApiProtocol,
 	getApiProtocol,
 	getModelId,
 	getModelId,
+	DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
+	isBlockingAsk,
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
 import { CloudService } from "@roo-code/cloud"
 import { CloudService } from "@roo-code/cloud"
@@ -96,24 +101,6 @@ import { AutoApprovalHandler } from "./AutoApprovalHandler"
 
 
 const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
 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 = {
 export type TaskOptions = {
 	provider: ClineProvider
 	provider: ClineProvider
 	apiConfiguration: ProviderSettings
 	apiConfiguration: ProviderSettings
@@ -132,7 +119,7 @@ export type TaskOptions = {
 	onCreated?: (task: Task) => void
 	onCreated?: (task: Task) => void
 }
 }
 
 
-export class Task extends EventEmitter<TaskEvents> {
+export class Task extends EventEmitter<TaskEvents> implements TaskLike {
 	todoList?: TodoItem[]
 	todoList?: TodoItem[]
 	readonly taskId: string
 	readonly taskId: string
 	readonly instanceId: string
 	readonly instanceId: string
@@ -189,6 +176,7 @@ export class Task extends EventEmitter<TaskEvents> {
 	providerRef: WeakRef<ClineProvider>
 	providerRef: WeakRef<ClineProvider>
 	private readonly globalStoragePath: string
 	private readonly globalStoragePath: string
 	abort: boolean = false
 	abort: boolean = false
+	blockingAsk?: BlockingAsk
 	didFinishAbortingStream = false
 	didFinishAbortingStream = false
 	abandoned = false
 	abandoned = false
 	isInitialized = false
 	isInitialized = false
@@ -545,7 +533,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		this.clineMessages.push(message)
 		this.clineMessages.push(message)
 		const provider = this.providerRef.deref()
 		const provider = this.providerRef.deref()
 		await provider?.postStateToWebview()
 		await provider?.postStateToWebview()
-		this.emit("message", { action: "created", message })
+		this.emit(RooCodeEventName.Message, { action: "created", message })
 		await this.saveClineMessages()
 		await this.saveClineMessages()
 
 
 		const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()
 		const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()
@@ -567,7 +555,7 @@ export class Task extends EventEmitter<TaskEvents> {
 	private async updateClineMessage(message: ClineMessage) {
 	private async updateClineMessage(message: ClineMessage) {
 		const provider = this.providerRef.deref()
 		const provider = this.providerRef.deref()
 		await provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message })
 		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()
 		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
 				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)
 			await this.providerRef.deref()?.updateTaskHistory(historyItem)
 		} catch (error) {
 		} catch (error) {
@@ -702,7 +690,17 @@ export class Task extends EventEmitter<TaskEvents> {
 			await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
 			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 })
 		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) {
 		if (this.lastMessageTs !== askTs) {
 			// Could happen if we send multiple asks in a row i.e. with
 			// 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.askResponse = undefined
 		this.askResponseText = undefined
 		this.askResponseText = undefined
 		this.askResponseImages = 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
 		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.askResponse = askResponse
 		this.askResponseText = text
 		this.askResponseText = text
 		this.askResponseImages = images
 		this.askResponseImages = images
@@ -947,7 +956,7 @@ export class Task extends EventEmitter<TaskEvents> {
 	public async resumePausedTask(lastMessage: string) {
 	public async resumePausedTask(lastMessage: string) {
 		// Release this Cline instance from paused state.
 		// Release this Cline instance from paused state.
 		this.isPaused = false
 		this.isPaused = false
-		this.emit("taskUnpaused")
+		this.emit(RooCodeEventName.TaskUnpaused)
 
 
 		// Fake an answer from the subtask that it has completed running and
 		// 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
 		// 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)
 			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(
 		const lastApiReqStartedIndex = findLastIndex(
 			modifiedClineMessages,
 			modifiedClineMessages,
 			(m) => m.type === "say" && m.say === "api_req_started",
 			(m) => m.type === "say" && m.say === "api_req_started",
@@ -990,6 +1002,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		if (lastApiReqStartedIndex !== -1) {
 		if (lastApiReqStartedIndex !== -1) {
 			const lastApiReqStarted = modifiedClineMessages[lastApiReqStartedIndex]
 			const lastApiReqStarted = modifiedClineMessages[lastApiReqStartedIndex]
 			const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}")
 			const { cost, cancelReason }: ClineApiReqInfo = JSON.parse(lastApiReqStarted.text || "{}")
+
 			if (cost === undefined && cancelReason === undefined) {
 			if (cost === undefined && cancelReason === undefined) {
 				modifiedClineMessages.splice(lastApiReqStartedIndex, 1)
 				modifiedClineMessages.splice(lastApiReqStartedIndex, 1)
 			}
 			}
@@ -1009,7 +1022,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		const lastClineMessage = this.clineMessages
 		const lastClineMessage = this.clineMessages
 			.slice()
 			.slice()
 			.reverse()
 			.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
 		let askType: ClineAsk
 		if (lastClineMessage?.ask === "completion_result") {
 		if (lastClineMessage?.ask === "completion_result") {
@@ -1020,9 +1033,11 @@ export class Task extends EventEmitter<TaskEvents> {
 
 
 		this.isInitialized = true
 		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 responseText: string | undefined
 		let responseImages: string[] | undefined
 		let responseImages: string[] | undefined
+
 		if (response === "messageResponse") {
 		if (response === "messageResponse") {
 			await this.say("user_feedback", text, images)
 			await this.say("user_feedback", text, images)
 			responseText = text
 			responseText = text
@@ -1200,6 +1215,8 @@ export class Task extends EventEmitter<TaskEvents> {
 	}
 	}
 
 
 	public dispose(): void {
 	public dispose(): void {
+		console.log(`[Task] disposing task ${this.taskId}.${this.instanceId}`)
+
 		// Stop waiting for child task completion.
 		// Stop waiting for child task completion.
 		if (this.pauseInterval) {
 		if (this.pauseInterval) {
 			clearInterval(this.pauseInterval)
 			clearInterval(this.pauseInterval)
@@ -1261,7 +1278,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		}
 		}
 
 
 		this.abort = true
 		this.abort = true
-		this.emit("taskAborted")
+		this.emit(RooCodeEventName.TaskAborted)
 
 
 		try {
 		try {
 			this.dispose() // Call the centralized dispose method
 			this.dispose() // Call the centralized dispose method
@@ -1303,11 +1320,11 @@ export class Task extends EventEmitter<TaskEvents> {
 		let nextUserContent = userContent
 		let nextUserContent = userContent
 		let includeFileDetails = true
 		let includeFileDetails = true
 
 
-		this.emit("taskStarted")
+		this.emit(RooCodeEventName.TaskStarted)
 
 
 		while (!this.abort) {
 		while (!this.abort) {
 			const didEndLoop = await this.recursivelyMakeClineRequests(nextUserContent, includeFileDetails)
 			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
 			// 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
 			// 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
 					// If this.abort is already true, it means the user clicked cancel, so we should
 					// treat this as "user_cancelled" rather than "streaming_failed"
 					// treat this as "user_cancelled" rather than "streaming_failed"
 					const cancelReason = this.abort ? "user_cancelled" : "streaming_failed"
 					const cancelReason = this.abort ? "user_cancelled" : "streaming_failed"
+
 					const streamingFailedMessage = this.abort
 					const streamingFailedMessage = this.abort
 						? undefined
 						? undefined
 						: (error.message ?? JSON.stringify(serializeError(error), null, 2))
 						: (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 this.abortTask()
-
 					await abortStream(cancelReason, streamingFailedMessage)
 					await abortStream(cancelReason, streamingFailedMessage)
 
 
 					const history = await provider?.getTaskWithId(this.taskId)
 					const history = await provider?.getTaskWithId(this.taskId)
@@ -2126,7 +2143,7 @@ export class Task extends EventEmitter<TaskEvents> {
 		this.toolUsage[toolName].failures++
 		this.toolUsage[toolName].failures++
 
 
 		if (error) {
 		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 Anthropic from "@anthropic-ai/sdk"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 
 
+import { RooCodeEventName } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
 
 
 import { Task } from "../task/Task"
 import { Task } from "../task/Task"
@@ -41,11 +42,13 @@ export async function attemptCompletionTool(
 	if (preventCompletionWithOpenTodos && hasIncompleteTodos) {
 	if (preventCompletionWithOpenTodos && hasIncompleteTodos) {
 		cline.consecutiveMistakeCount++
 		cline.consecutiveMistakeCount++
 		cline.recordToolError("attempt_completion")
 		cline.recordToolError("attempt_completion")
+
 		pushToolResult(
 		pushToolResult(
 			formatResponse.toolError(
 			formatResponse.toolError(
 				"Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.",
 				"Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.",
 			),
 			),
 		)
 		)
+
 		return
 		return
 	}
 	}
 
 
@@ -67,12 +70,12 @@ export async function attemptCompletionTool(
 					await cline.say("completion_result", removeClosingTag("result", result), undefined, false)
 					await cline.say("completion_result", removeClosingTag("result", result), undefined, false)
 
 
 					TelemetryService.instance.captureTaskCompleted(cline.taskId)
 					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(() => {})
 					await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
 				}
 				}
 			} else {
 			} else {
-				// no command, still outputting partial result
+				// No command, still outputting partial result
 				await cline.say("completion_result", removeClosingTag("result", result), undefined, block.partial)
 				await cline.say("completion_result", removeClosingTag("result", result), undefined, block.partial)
 			}
 			}
 			return
 			return
@@ -90,7 +93,7 @@ export async function attemptCompletionTool(
 			// Users must use execute_command tool separately before attempt_completion
 			// Users must use execute_command tool separately before attempt_completion
 			await cline.say("completion_result", result, undefined, false)
 			await cline.say("completion_result", result, undefined, false)
 			TelemetryService.instance.captureTaskCompleted(cline.taskId)
 			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) {
 			if (cline.parentTask) {
 				const didApprove = await askFinishSubTaskApproval()
 				const didApprove = await askFinishSubTaskApproval()

+ 4 - 2
src/core/tools/newTaskTool.ts

@@ -1,5 +1,7 @@
 import delay from "delay"
 import delay from "delay"
 
 
+import { RooCodeEventName } from "@roo-code/types"
+
 import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
 import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
 import { Task } from "../task/Task"
 import { Task } from "../task/Task"
 import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
 import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
@@ -93,14 +95,14 @@ export async function newTaskTool(
 			// Delay to allow mode change to take effect
 			// Delay to allow mode change to take effect
 			await delay(500)
 			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}`)
 			pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`)
 
 
 			// Set the isPaused flag to true so the parent
 			// Set the isPaused flag to true so the parent
 			// task can wait for the sub-task to finish.
 			// task can wait for the sub-task to finish.
 			cline.isPaused = true
 			cline.isPaused = true
-			cline.emit("taskPaused")
+			cline.emit(RooCodeEventName.TaskPaused)
 
 
 			return
 			return
 		}
 		}

+ 56 - 36
src/core/webview/ClineProvider.ts

@@ -10,6 +10,8 @@ import pWaitFor from "p-wait-for"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 
 
 import {
 import {
+	type TaskProviderLike,
+	type TaskProviderEvents,
 	type GlobalState,
 	type GlobalState,
 	type ProviderName,
 	type ProviderName,
 	type ProviderSettings,
 	type ProviderSettings,
@@ -24,6 +26,7 @@ import {
 	type TerminalActionPromptType,
 	type TerminalActionPromptType,
 	type HistoryItem,
 	type HistoryItem,
 	type CloudUserInfo,
 	type CloudUserInfo,
+	RooCodeEventName,
 	requestyDefaultModelId,
 	requestyDefaultModelId,
 	openRouterDefaultModelId,
 	openRouterDefaultModelId,
 	glamaDefaultModelId,
 	glamaDefaultModelId,
@@ -34,8 +37,6 @@ import {
 import { TelemetryService } from "@roo-code/telemetry"
 import { TelemetryService } from "@roo-code/telemetry"
 import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
 import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
 
 
-import { t } from "../../i18n"
-import { setPanel } from "../../activate/registerCommands"
 import { Package } from "../../shared/package"
 import { Package } from "../../shared/package"
 import { findLast } from "../../shared/array"
 import { findLast } from "../../shared/array"
 import { supportPrompt } from "../../shared/support-prompt"
 import { supportPrompt } from "../../shared/support-prompt"
@@ -44,10 +45,15 @@ import { ExtensionMessage, MarketplaceInstalledMetadata } from "../../shared/Ext
 import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
 import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes"
 import { experimentDefault } from "../../shared/experiments"
 import { experimentDefault } from "../../shared/experiments"
 import { formatLanguage } from "../../shared/language"
 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 { Terminal } from "../../integrations/terminal/Terminal"
 import { downloadTask } from "../../integrations/misc/export-markdown"
 import { downloadTask } from "../../integrations/misc/export-markdown"
 import { getTheme } from "../../integrations/theme/getTheme"
 import { getTheme } from "../../integrations/theme/getTheme"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
 import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
+
 import { McpHub } from "../../services/mcp/McpHub"
 import { McpHub } from "../../services/mcp/McpHub"
 import { McpServerManager } from "../../services/mcp/McpServerManager"
 import { McpServerManager } from "../../services/mcp/McpServerManager"
 import { MarketplaceManager } from "../../services/marketplace"
 import { MarketplaceManager } from "../../services/marketplace"
@@ -55,36 +61,37 @@ import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckp
 import { CodeIndexManager } from "../../services/code-index/manager"
 import { CodeIndexManager } from "../../services/code-index/manager"
 import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
 import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
 import { MdmService } from "../../services/mdm/MdmService"
 import { MdmService } from "../../services/mdm/MdmService"
+
 import { fileExistsAtPath } from "../../utils/fs"
 import { fileExistsAtPath } from "../../utils/fs"
 import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
 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 { ContextProxy } from "../config/ContextProxy"
 import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
 import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
 import { CustomModesManager } from "../config/CustomModesManager"
 import { CustomModesManager } from "../config/CustomModesManager"
-import { buildApiHandler } from "../../api"
 import { Task, TaskOptions } from "../task/Task"
 import { Task, TaskOptions } from "../task/Task"
-import { getNonce } from "./getNonce"
-import { getUri } from "./getUri"
 import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
 import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
-import { getWorkspacePath } from "../../utils/path"
+
 import { webviewMessageHandler } from "./webviewMessageHandler"
 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/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
  * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
  */
  */
 
 
-export type ClineProviderEvents = {
-	taskCreated: [task: Task]
-}
-
 export class ClineProvider
 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
 	// 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
 	// 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)
 		this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
 
 
-		// Initialize cloud profile sync
+		// Initialize Roo Code Cloud profile sync.
 		this.initializeCloudProfileSync().catch((error) => {
 		this.initializeCloudProfileSync().catch((error) => {
 			this.log(`Failed to initialize cloud profile sync: ${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).
 	// 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.
 	// 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.
 		// 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.
 		// Ensure getState() resolves correctly.
 		const state = await this.getState()
 		const state = await this.getState()
@@ -247,7 +255,8 @@ export class ClineProvider
 	}
 	}
 
 
 	async performPreparationTasks(cline: Task) {
 	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") {
 		if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") {
 			try {
 			try {
 				if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) {
 				if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) {
@@ -271,24 +280,26 @@ export class ClineProvider
 		}
 		}
 
 
 		// Pop the top Cline instance from the stack.
 		// 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 {
 			try {
 				// Abort the running task and set isAbandoned to true so
 				// Abort the running task and set isAbandoned to true so
 				// all running promises will exit as well.
 				// all running promises will exit as well.
-				await cline.abortTask(true)
+				await task.abortTask(true)
 			} catch (e) {
 			} catch (e) {
 				this.log(
 				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
 			// Make sure no reference kept, once promises end it will be
 			// garbage collected.
 			// garbage collected.
-			cline = undefined
+			task = undefined
 		}
 		}
 	}
 	}
 
 
@@ -343,8 +354,13 @@ export class ClineProvider
 
 
 	async dispose() {
 	async dispose() {
 		this.log("Disposing ClineProvider...")
 		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) {
 		if (this.view && "dispose" in this.view) {
 			this.view.dispose()
 			this.view.dispose()
@@ -375,6 +391,9 @@ export class ClineProvider
 		this.log("Disposed all disposables")
 		this.log("Disposed all disposables")
 		ClineProvider.activeInstances.delete(this)
 		ClineProvider.activeInstances.delete(this)
 
 
+		// Clean up any event listeners attached to this provider
+		this.removeAllListeners()
+
 		McpServerManager.unregisterProvider(this)
 		McpServerManager.unregisterProvider(this)
 	}
 	}
 
 
@@ -403,6 +422,7 @@ export class ClineProvider
 
 
 	public static async isActiveTask(): Promise<boolean> {
 	public static async isActiveTask(): Promise<boolean> {
 		const visibleProvider = await ClineProvider.getInstance()
 		const visibleProvider = await ClineProvider.getInstance()
+
 		if (!visibleProvider) {
 		if (!visibleProvider) {
 			return false
 			return false
 		}
 		}
@@ -653,7 +673,7 @@ export class ClineProvider
 			rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
 			rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
 			parentTask,
 			parentTask,
 			taskNumber: this.clineStack.length + 1,
 			taskNumber: this.clineStack.length + 1,
-			onCreated: (instance) => this.emit("taskCreated", instance),
+			onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance),
 			...options,
 			...options,
 		})
 		})
 
 
@@ -732,7 +752,7 @@ export class ClineProvider
 			rootTask: historyItem.rootTask,
 			rootTask: historyItem.rootTask,
 			parentTask: historyItem.parentTask,
 			parentTask: historyItem.parentTask,
 			taskNumber: historyItem.number,
 			taskNumber: historyItem.number,
-			onCreated: (instance) => this.emit("taskCreated", instance),
+			onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance),
 		})
 		})
 
 
 		await this.addClineToStack(task)
 		await this.addClineToStack(task)
@@ -942,7 +962,7 @@ export class ClineProvider
 
 
 		if (cline) {
 		if (cline) {
 			TelemetryService.instance.captureModeSwitch(cline.taskId, newMode)
 			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
 			// Store the current mode in case we need to rollback
 			const previousMode = (cline as any)._taskMode
 			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 { safeWriteJson } from "../../../utils/safeWriteJson"
 
 
 import { ClineProvider } from "../ClineProvider"
 import { ClineProvider } from "../ClineProvider"
-import { AsyncInvokeOutputDataConfig } from "@aws-sdk/client-bedrock-runtime"
 
 
 // Mock setup must come before imports
 // Mock setup must come before imports
 vi.mock("../../prompts/sections/custom-instructions")
 vi.mock("../../prompts/sections/custom-instructions")
@@ -215,6 +214,7 @@ vi.mock("../../task/Task", () => ({
 				setParentTask: vi.fn(),
 				setParentTask: vi.fn(),
 				setRootTask: vi.fn(),
 				setRootTask: vi.fn(),
 				taskId: taskId || "test-task-id",
 				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 () => {
 		it("should handle errors during mode switch gracefully", async () => {
 			await provider.resolveWebviewView(mockWebviewView)
 			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 = {
 			const mockTask = {
 				taskId: "test-task-id",
 				taskId: "test-task-id",
 				_taskMode: "code",
 				_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(),
 				saveClineMessages: vi.fn(),
 				clineMessages: [],
 				clineMessages: [],
@@ -915,13 +921,42 @@ describe("ClineProvider - Sticky Mode", () => {
 			// Add task to provider stack
 			// Add task to provider stack
 			await provider.addClineToStack(mockTask as any)
 			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
 			// Mock console.error to suppress error output
 			const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
 			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
 			// 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")
 			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()
 			consoleErrorSpy.mockRestore()
 		})
 		})
 
 

+ 68 - 41
src/extension/api.ts

@@ -5,22 +5,21 @@ import * as path from "path"
 import * as os from "os"
 import * as os from "os"
 
 
 import {
 import {
-	RooCodeAPI,
-	RooCodeSettings,
-	RooCodeEvents,
+	type RooCodeAPI,
+	type RooCodeSettings,
+	type RooCodeEvents,
+	type ProviderSettings,
+	type ProviderSettingsEntry,
+	type TaskEvent,
 	RooCodeEventName,
 	RooCodeEventName,
-	ProviderSettings,
-	ProviderSettingsEntry,
+	TaskCommandName,
 	isSecretStateKey,
 	isSecretStateKey,
 	IpcOrigin,
 	IpcOrigin,
 	IpcMessageType,
 	IpcMessageType,
-	TaskCommandName,
-	TaskEvent,
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { IpcServer } from "@roo-code/ipc"
 import { IpcServer } from "@roo-code/ipc"
 
 
 import { Package } from "../shared/package"
 import { Package } from "../shared/package"
-import { getWorkspacePath } from "../utils/path"
 import { ClineProvider } from "../core/webview/ClineProvider"
 import { ClineProvider } from "../core/webview/ClineProvider"
 import { openClineInNewTab } from "../activate/registerCommands"
 import { openClineInNewTab } from "../activate/registerCommands"
 
 
@@ -214,58 +213,86 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 	}
 	}
 
 
 	private registerListeners(provider: ClineProvider) {
 	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": {
 		case "cerebras": {
 			const id = apiConfiguration.apiModelId ?? cerebrasDefaultModelId
 			const id = apiConfiguration.apiModelId ?? cerebrasDefaultModelId
 			const info = cerebrasModels[id as keyof typeof cerebrasModels]
 			const info = cerebrasModels[id as keyof typeof cerebrasModels]
-      return { id, info }
+			return { id, info }
 		}
 		}
 		case "sambanova": {
 		case "sambanova": {
 			const id = apiConfiguration.apiModelId ?? sambaNovaDefaultModelId
 			const id = apiConfiguration.apiModelId ?? sambaNovaDefaultModelId