Browse Source

Add some functionality to @roo-code/core for the cli (#10584)

Chris Estreich 2 months ago
parent
commit
a4eb15b5a8
60 changed files with 1344 additions and 633 deletions
  1. 5 1
      packages/core/package.json
  2. 6 0
      packages/core/src/browser.ts
  3. 6 0
      packages/core/src/cli.ts
  4. 91 0
      packages/core/src/debug-log/index.ts
  5. 2 0
      packages/core/src/index.ts
  6. 122 0
      packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts
  7. 145 0
      packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts
  8. 246 0
      packages/core/src/message-utils/__tests__/consolidateTokenUsage.spec.ts
  9. 90 0
      packages/core/src/message-utils/consolidateApiRequests.ts
  10. 160 0
      packages/core/src/message-utils/consolidateCommands.ts
  11. 157 0
      packages/core/src/message-utils/consolidateTokenUsage.ts
  12. 12 0
      packages/core/src/message-utils/index.ts
  13. 2 2
      packages/core/src/message-utils/safeJsonParse.ts
  14. 22 0
      packages/types/src/embedding.ts
  15. 1 0
      packages/types/src/index.ts
  16. 137 1
      packages/types/src/vscode-extension-host.ts
  17. 8 0
      packages/vscode-shim/src/api/create-vscode-api-mock.ts
  18. 1 1
      src/api/providers/anthropic-vertex.ts
  19. 1 2
      src/api/providers/gemini.ts
  20. 1 1
      src/core/auto-approval/index.ts
  21. 1 1
      src/core/auto-approval/tools.ts
  22. 1 1
      src/core/checkpoints/index.ts
  23. 2 1
      src/core/task/Task.ts
  24. 3 3
      src/core/tools/ApplyDiffTool.ts
  25. 2 2
      src/core/tools/ApplyPatchTool.ts
  26. 2 1
      src/core/tools/AskFollowupQuestionTool.ts
  27. 2 1
      src/core/tools/AttemptCompletionTool.ts
  28. 2 1
      src/core/tools/BaseTool.ts
  29. 5 7
      src/core/tools/BrowserActionTool.ts
  30. 2 1
      src/core/tools/CodebaseSearchTool.ts
  31. 4 3
      src/core/tools/EditFileTool.ts
  32. 4 2
      src/core/tools/FetchInstructionsTool.ts
  33. 4 2
      src/core/tools/ListFilesTool.ts
  34. 1 3
      src/core/tools/MultiApplyDiffTool.ts
  35. 4 3
      src/core/tools/ReadFileTool.ts
  36. 4 3
      src/core/tools/SearchAndReplaceTool.ts
  37. 4 2
      src/core/tools/SearchFilesTool.ts
  38. 4 3
      src/core/tools/SearchReplaceTool.ts
  39. 4 3
      src/core/tools/UseMcpToolTool.ts
  40. 4 4
      src/core/tools/WriteToFileTool.ts
  41. 1 3
      src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts
  42. 3 1
      src/core/tools/accessMcpResourceTool.ts
  43. 2 2
      src/integrations/editor/DiffViewProvider.ts
  44. 4 1
      src/services/browser/BrowserSession.ts
  45. 13 7
      src/services/code-index/service-factory.ts
  46. 0 136
      src/shared/ExtensionMessage.ts
  47. 2 83
      src/shared/combineApiRequests.ts
  48. 2 145
      src/shared/combineCommandSequences.ts
  49. 1 0
      src/shared/core.ts
  50. 1 22
      src/shared/embeddingModels.ts
  51. 8 156
      src/shared/getApiMetrics.ts
  52. 2 0
      src/shared/todo.ts
  53. 7 5
      webview-ui/src/components/chat/BrowserActionRow.tsx
  54. 1 2
      webview-ui/src/components/chat/BrowserSessionRow.tsx
  55. 10 3
      webview-ui/src/components/chat/ChatRow.tsx
  56. 1 2
      webview-ui/src/components/chat/ChatView.tsx
  57. 1 4
      webview-ui/src/components/chat/CodeIndexPopover.tsx
  58. 1 1
      webview-ui/src/components/chat/CommandExecution.tsx
  59. 2 2
      webview-ui/src/components/chat/IndexingStatusBadge.tsx
  60. 8 3
      webview-ui/src/components/chat/McpExecution.tsx

+ 5 - 1
packages/core/package.json

@@ -3,7 +3,11 @@
 	"description": "Platform agnostic core functionality for Roo Code.",
 	"version": "0.0.0",
 	"type": "module",
-	"exports": "./src/index.ts",
+	"exports": {
+		".": "./src/index.ts",
+		"./cli": "./src/cli.ts",
+		"./browser": "./src/browser.ts"
+	},
 	"scripts": {
 		"lint": "eslint src --ext=ts --max-warnings=0",
 		"check-types": "tsc --noEmit",

+ 6 - 0
packages/core/src/browser.ts

@@ -0,0 +1,6 @@
+/**
+ * Browser-safe exports for the core package. These can safely be used
+ * in browser environments like `webview-ui`.
+ */
+
+export * from "./message-utils/index.js"

+ 6 - 0
packages/core/src/cli.ts

@@ -0,0 +1,6 @@
+/**
+ * Cli-safe exports for the core package.
+ */
+
+export * from "./debug-log/index.js"
+export * from "./message-utils/index.js"

+ 91 - 0
packages/core/src/debug-log/index.ts

@@ -0,0 +1,91 @@
+/**
+ * File-based debug logging utility
+ *
+ * This writes logs to ~/.roo/cli-debug.log, avoiding stdout/stderr
+ * which would break TUI applications. The log format is timestamped JSON.
+ *
+ * Usage:
+ *   import { debugLog, DebugLogger } from "@roo-code/core/debug-log"
+ *
+ *   // Simple logging
+ *   debugLog("handleModeSwitch", { mode: newMode, configId })
+ *
+ *   // Or create a named logger for a component
+ *   const log = new DebugLogger("ClineProvider")
+ *   log.info("handleModeSwitch", { mode: newMode })
+ */
+
+import * as fs from "fs"
+import * as path from "path"
+import * as os from "os"
+
+const DEBUG_LOG_PATH = path.join(os.homedir(), ".roo", "cli-debug.log")
+
+/**
+ * Simple file-based debug log function.
+ * Writes timestamped entries to ~/.roo/cli-debug.log
+ */
+export function debugLog(message: string, data?: unknown): void {
+	try {
+		const logDir = path.dirname(DEBUG_LOG_PATH)
+
+		if (!fs.existsSync(logDir)) {
+			fs.mkdirSync(logDir, { recursive: true })
+		}
+
+		const timestamp = new Date().toISOString()
+
+		const entry = data
+			? `[${timestamp}] ${message}: ${JSON.stringify(data, null, 2)}\n`
+			: `[${timestamp}] ${message}\n`
+
+		fs.appendFileSync(DEBUG_LOG_PATH, entry)
+	} catch {
+		// NO-OP - don't let logging errors break functionality
+	}
+}
+
+/**
+ * Debug logger with component context.
+ * Prefixes all messages with the component name.
+ */
+export class DebugLogger {
+	private component: string
+
+	constructor(component: string) {
+		this.component = component
+	}
+
+	/**
+	 * Log a debug message with optional data
+	 */
+	debug(message: string, data?: unknown): void {
+		debugLog(`[${this.component}] ${message}`, data)
+	}
+
+	/**
+	 * Alias for debug
+	 */
+	info(message: string, data?: unknown): void {
+		this.debug(message, data)
+	}
+
+	/**
+	 * Log a warning
+	 */
+	warn(message: string, data?: unknown): void {
+		debugLog(`[${this.component}] WARN: ${message}`, data)
+	}
+
+	/**
+	 * Log an error
+	 */
+	error(message: string, data?: unknown): void {
+		debugLog(`[${this.component}] ERROR: ${message}`, data)
+	}
+}
+
+/**
+ * Pre-configured logger for provider/mode debugging
+ */
+export const providerDebugLog = new DebugLogger("ProviderSettings")

+ 2 - 0
packages/core/src/index.ts

@@ -1 +1,3 @@
 export * from "./custom-tools/index.js"
+export * from "./debug-log/index.js"
+export * from "./message-utils/index.js"

+ 122 - 0
packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts

@@ -0,0 +1,122 @@
+// npx vitest run packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts
+
+import type { ClineMessage } from "@roo-code/types"
+
+import { consolidateApiRequests } from "../consolidateApiRequests.js"
+
+describe("consolidateApiRequests", () => {
+	// Helper function to create a basic api_req_started message
+	const createApiReqStarted = (ts: number, data: Record<string, unknown> = {}): ClineMessage => ({
+		ts,
+		type: "say",
+		say: "api_req_started",
+		text: JSON.stringify(data),
+	})
+
+	// Helper function to create a basic api_req_finished message
+	const createApiReqFinished = (ts: number, data: Record<string, unknown> = {}): ClineMessage => ({
+		ts,
+		type: "say",
+		say: "api_req_finished",
+		text: JSON.stringify(data),
+	})
+
+	// Helper function to create a regular text message
+	const createTextMessage = (ts: number, text: string): ClineMessage => ({
+		ts,
+		type: "say",
+		say: "text",
+		text,
+	})
+
+	it("should consolidate a matching pair of api_req_started and api_req_finished messages", () => {
+		const messages: ClineMessage[] = [
+			createApiReqStarted(1000, { request: "GET /api/data" }),
+			createApiReqFinished(1001, { cost: 0.005 }),
+		]
+
+		const result = consolidateApiRequests(messages)
+
+		expect(result.length).toBe(1)
+		expect(result[0]!.say).toBe("api_req_started")
+
+		const parsedText = JSON.parse(result[0]!.text || "{}")
+		expect(parsedText.request).toBe("GET /api/data")
+		expect(parsedText.cost).toBe(0.005)
+	})
+
+	it("should handle messages with no api_req pairs", () => {
+		const messages: ClineMessage[] = [createTextMessage(1000, "Hello"), createTextMessage(1001, "World")]
+
+		const result = consolidateApiRequests(messages)
+
+		expect(result).toEqual(messages)
+	})
+
+	it("should handle empty messages array", () => {
+		const result = consolidateApiRequests([])
+		expect(result).toEqual([])
+	})
+
+	it("should handle single message array", () => {
+		const messages: ClineMessage[] = [createTextMessage(1000, "Hello")]
+		const result = consolidateApiRequests(messages)
+		expect(result).toEqual(messages)
+	})
+
+	it("should preserve non-api messages in the result", () => {
+		const messages: ClineMessage[] = [
+			createTextMessage(1000, "Before"),
+			createApiReqStarted(1001, { request: "test" }),
+			createApiReqFinished(1002, { cost: 0.01 }),
+			createTextMessage(1003, "After"),
+		]
+
+		const result = consolidateApiRequests(messages)
+
+		expect(result.length).toBe(3)
+		expect(result[0]!.text).toBe("Before")
+		expect(result[1]!.say).toBe("api_req_started")
+		expect(result[2]!.text).toBe("After")
+	})
+
+	it("should handle multiple api_req pairs", () => {
+		const messages: ClineMessage[] = [
+			createApiReqStarted(1000, { request: "first" }),
+			createApiReqFinished(1001, { cost: 0.01 }),
+			createApiReqStarted(1002, { request: "second" }),
+			createApiReqFinished(1003, { cost: 0.02 }),
+		]
+
+		const result = consolidateApiRequests(messages)
+
+		expect(result.length).toBe(2)
+		expect(JSON.parse(result[0]!.text || "{}").request).toBe("first")
+		expect(JSON.parse(result[1]!.text || "{}").request).toBe("second")
+	})
+
+	it("should handle orphan api_req_started without finish", () => {
+		const messages: ClineMessage[] = [
+			createApiReqStarted(1000, { request: "orphan" }),
+			createTextMessage(1001, "Text"),
+		]
+
+		const result = consolidateApiRequests(messages)
+
+		expect(result.length).toBe(2)
+		expect(result[0]!.say).toBe("api_req_started")
+		expect(JSON.parse(result[0]!.text || "{}").request).toBe("orphan")
+	})
+
+	it("should handle invalid JSON in message text", () => {
+		const messages: ClineMessage[] = [
+			{ ts: 1000, type: "say", say: "api_req_started", text: "invalid json" },
+			createApiReqFinished(1001, { cost: 0.01 }),
+		]
+
+		const result = consolidateApiRequests(messages)
+
+		// Should still consolidate, merging what it can
+		expect(result.length).toBe(1)
+	})
+})

+ 145 - 0
packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts

@@ -0,0 +1,145 @@
+// npx vitest run packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts
+
+import type { ClineMessage } from "@roo-code/types"
+
+import { consolidateCommands, COMMAND_OUTPUT_STRING } from "../consolidateCommands.js"
+
+describe("consolidateCommands", () => {
+	describe("command sequences", () => {
+		it("should consolidate command and command_output messages", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1000 },
+				{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1001 },
+				{ type: "ask", ask: "command_output", text: "file2.txt", ts: 1002 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(1)
+			expect(result[0]!.ask).toBe("command")
+			expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}file1.txt\nfile2.txt`)
+		})
+
+		it("should handle multiple command sequences", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1000 },
+				{ type: "ask", ask: "command_output", text: "output1", ts: 1001 },
+				{ type: "ask", ask: "command", text: "pwd", ts: 1002 },
+				{ type: "ask", ask: "command_output", text: "output2", ts: 1003 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(2)
+			expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}output1`)
+			expect(result[1]!.text).toBe(`pwd\n${COMMAND_OUTPUT_STRING}output2`)
+		})
+
+		it("should handle command without output", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1000 },
+				{ type: "say", say: "text", text: "some text", ts: 1001 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(2)
+			expect(result[0]!.ask).toBe("command")
+			expect(result[0]!.text).toBe("ls")
+			expect(result[1]!.say).toBe("text")
+		})
+
+		it("should handle duplicate outputs (ask and say with same text)", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1000 },
+				{ type: "ask", ask: "command_output", text: "same output", ts: 1001 },
+				{ type: "say", say: "command_output", text: "same output", ts: 1002 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(1)
+			expect(result[0]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}same output`)
+		})
+	})
+
+	describe("MCP server sequences", () => {
+		it("should consolidate use_mcp_server and mcp_server_response messages", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({ server: "test", tool: "myTool" }),
+					ts: 1000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "response data", ts: 1001 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(1)
+			expect(result[0]!.ask).toBe("use_mcp_server")
+			const parsed = JSON.parse(result[0]!.text || "{}")
+			expect(parsed.server).toBe("test")
+			expect(parsed.response).toBe("response data")
+		})
+
+		it("should handle MCP request without response", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({ server: "test" }),
+					ts: 1000,
+				},
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(1)
+			expect(result[0]!.ask).toBe("use_mcp_server")
+		})
+
+		it("should handle multiple MCP responses", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({ server: "test" }),
+					ts: 1000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "response1", ts: 1001 },
+				{ type: "say", say: "mcp_server_response", text: "response2", ts: 1002 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(1)
+			const parsed = JSON.parse(result[0]!.text || "{}")
+			expect(parsed.response).toBe("response1\nresponse2")
+		})
+	})
+
+	describe("mixed messages", () => {
+		it("should preserve non-command, non-MCP messages", () => {
+			const messages: ClineMessage[] = [
+				{ type: "say", say: "text", text: "before", ts: 1000 },
+				{ type: "ask", ask: "command", text: "ls", ts: 1001 },
+				{ type: "ask", ask: "command_output", text: "output", ts: 1002 },
+				{ type: "say", say: "text", text: "after", ts: 1003 },
+			]
+
+			const result = consolidateCommands(messages)
+
+			expect(result.length).toBe(3)
+			expect(result[0]!.text).toBe("before")
+			expect(result[1]!.text).toBe(`ls\n${COMMAND_OUTPUT_STRING}output`)
+			expect(result[2]!.text).toBe("after")
+		})
+
+		it("should handle empty array", () => {
+			const result = consolidateCommands([])
+			expect(result).toEqual([])
+		})
+	})
+})

+ 246 - 0
packages/core/src/message-utils/__tests__/consolidateTokenUsage.spec.ts

@@ -0,0 +1,246 @@
+// npx vitest run packages/core/src/message-utils/__tests__/consolidateTokenUsage.spec.ts
+
+import type { ClineMessage } from "@roo-code/types"
+
+import { consolidateTokenUsage, hasTokenUsageChanged, hasToolUsageChanged } from "../consolidateTokenUsage.js"
+
+describe("consolidateTokenUsage", () => {
+	// Helper function to create a basic api_req_started message
+	const createApiReqMessage = (
+		ts: number,
+		data: {
+			tokensIn?: number
+			tokensOut?: number
+			cacheWrites?: number
+			cacheReads?: number
+			cost?: number
+		},
+	): ClineMessage => ({
+		ts,
+		type: "say",
+		say: "api_req_started",
+		text: JSON.stringify(data),
+	})
+
+	describe("basic token accumulation", () => {
+		it("should accumulate tokens from a single message", () => {
+			const messages: ClineMessage[] = [createApiReqMessage(1000, { tokensIn: 100, tokensOut: 50, cost: 0.01 })]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.totalTokensIn).toBe(100)
+			expect(result.totalTokensOut).toBe(50)
+			expect(result.totalCost).toBe(0.01)
+		})
+
+		it("should accumulate tokens from multiple messages", () => {
+			const messages: ClineMessage[] = [
+				createApiReqMessage(1000, { tokensIn: 100, tokensOut: 50, cost: 0.01 }),
+				createApiReqMessage(1001, { tokensIn: 200, tokensOut: 100, cost: 0.02 }),
+			]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.totalTokensIn).toBe(300)
+			expect(result.totalTokensOut).toBe(150)
+			expect(result.totalCost).toBeCloseTo(0.03)
+		})
+
+		it("should handle cache writes and reads", () => {
+			const messages: ClineMessage[] = [
+				createApiReqMessage(1000, { tokensIn: 100, tokensOut: 50, cacheWrites: 500, cacheReads: 200 }),
+			]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.totalCacheWrites).toBe(500)
+			expect(result.totalCacheReads).toBe(200)
+		})
+
+		it("should handle empty messages array", () => {
+			const result = consolidateTokenUsage([])
+
+			expect(result.totalTokensIn).toBe(0)
+			expect(result.totalTokensOut).toBe(0)
+			expect(result.totalCost).toBe(0)
+			expect(result.contextTokens).toBe(0)
+		})
+	})
+
+	describe("context tokens calculation", () => {
+		it("should calculate context tokens from the last API request", () => {
+			const messages: ClineMessage[] = [
+				createApiReqMessage(1000, { tokensIn: 100, tokensOut: 50 }),
+				createApiReqMessage(1001, { tokensIn: 200, tokensOut: 100 }),
+			]
+
+			const result = consolidateTokenUsage(messages)
+
+			// Context tokens = tokensIn + tokensOut from last message
+			expect(result.contextTokens).toBe(300) // 200 + 100
+		})
+
+		it("should handle condense_context messages for context tokens", () => {
+			const messages: ClineMessage[] = [
+				createApiReqMessage(1000, { tokensIn: 100, tokensOut: 50 }),
+				{
+					ts: 1001,
+					type: "say",
+					say: "condense_context",
+					contextCondense: { newContextTokens: 5000, cost: 0.05 },
+				} as ClineMessage,
+			]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.contextTokens).toBe(5000)
+			expect(result.totalCost).toBeCloseTo(0.05)
+		})
+	})
+
+	describe("invalid data handling", () => {
+		it("should handle messages with invalid JSON", () => {
+			const messages: ClineMessage[] = [{ ts: 1000, type: "say", say: "api_req_started", text: "invalid json" }]
+
+			// Should not throw
+			const result = consolidateTokenUsage(messages)
+			expect(result.totalTokensIn).toBe(0)
+		})
+
+		it("should skip non-api_req_started messages", () => {
+			const messages: ClineMessage[] = [
+				{ ts: 1000, type: "say", say: "text", text: "hello" },
+				createApiReqMessage(1001, { tokensIn: 100, tokensOut: 50 }),
+			]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.totalTokensIn).toBe(100)
+			expect(result.totalTokensOut).toBe(50)
+		})
+
+		it("should handle missing token values", () => {
+			const messages: ClineMessage[] = [createApiReqMessage(1000, { cost: 0.01 })]
+
+			const result = consolidateTokenUsage(messages)
+
+			expect(result.totalTokensIn).toBe(0)
+			expect(result.totalTokensOut).toBe(0)
+			expect(result.totalCost).toBe(0.01)
+		})
+	})
+})
+
+describe("hasTokenUsageChanged", () => {
+	it("should return true when snapshot is undefined", () => {
+		const current = {
+			totalTokensIn: 100,
+			totalTokensOut: 50,
+			totalCost: 0.01,
+			contextTokens: 150,
+		}
+
+		expect(hasTokenUsageChanged(current, undefined)).toBe(true)
+	})
+
+	it("should return false when values are the same", () => {
+		const current = {
+			totalTokensIn: 100,
+			totalTokensOut: 50,
+			totalCost: 0.01,
+			contextTokens: 150,
+		}
+		const snapshot = { ...current }
+
+		expect(hasTokenUsageChanged(current, snapshot)).toBe(false)
+	})
+
+	it("should return true when totalTokensIn changes", () => {
+		const current = {
+			totalTokensIn: 200,
+			totalTokensOut: 50,
+			totalCost: 0.01,
+			contextTokens: 150,
+		}
+		const snapshot = {
+			totalTokensIn: 100,
+			totalTokensOut: 50,
+			totalCost: 0.01,
+			contextTokens: 150,
+		}
+
+		expect(hasTokenUsageChanged(current, snapshot)).toBe(true)
+	})
+
+	it("should return true when totalCost changes", () => {
+		const current = {
+			totalTokensIn: 100,
+			totalTokensOut: 50,
+			totalCost: 0.02,
+			contextTokens: 150,
+		}
+		const snapshot = {
+			totalTokensIn: 100,
+			totalTokensOut: 50,
+			totalCost: 0.01,
+			contextTokens: 150,
+		}
+
+		expect(hasTokenUsageChanged(current, snapshot)).toBe(true)
+	})
+})
+
+describe("hasToolUsageChanged", () => {
+	it("should return true when snapshot is undefined", () => {
+		const current = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+
+		expect(hasToolUsageChanged(current, undefined)).toBe(true)
+	})
+
+	it("should return false when values are the same", () => {
+		const current = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+		const snapshot = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+
+		expect(hasToolUsageChanged(current, snapshot)).toBe(false)
+	})
+
+	it("should return true when a tool is added", () => {
+		const current = {
+			read_file: { attempts: 1, failures: 0 },
+			write_to_file: { attempts: 1, failures: 0 },
+		}
+		const snapshot = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+
+		expect(hasToolUsageChanged(current, snapshot)).toBe(true)
+	})
+
+	it("should return true when attempts change", () => {
+		const current = {
+			read_file: { attempts: 2, failures: 0 },
+		}
+		const snapshot = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+
+		expect(hasToolUsageChanged(current, snapshot)).toBe(true)
+	})
+
+	it("should return true when failures change", () => {
+		const current = {
+			read_file: { attempts: 1, failures: 1 },
+		}
+		const snapshot = {
+			read_file: { attempts: 1, failures: 0 },
+		}
+
+		expect(hasToolUsageChanged(current, snapshot)).toBe(true)
+	})
+})

+ 90 - 0
packages/core/src/message-utils/consolidateApiRequests.ts

@@ -0,0 +1,90 @@
+import type { ClineMessage } from "@roo-code/types"
+
+/**
+ * Consolidates API request start and finish messages in an array of ClineMessages.
+ *
+ * This function looks for pairs of 'api_req_started' and 'api_req_finished' messages.
+ * When it finds a pair, it consolidates them into a single message.
+ * The JSON data in the text fields of both messages are merged.
+ *
+ * @param messages - An array of ClineMessage objects to process.
+ * @returns A new array of ClineMessage objects with API requests consolidated.
+ *
+ * @example
+ * const messages = [
+ *   { type: "say", say: "api_req_started", text: '{"request":"GET /api/data"}', ts: 1000 },
+ *   { type: "say", say: "api_req_finished", text: '{"cost":0.005}', ts: 1001 }
+ * ];
+ * const result = consolidateApiRequests(messages);
+ * // Result: [{ type: "say", say: "api_req_started", text: '{"request":"GET /api/data","cost":0.005}', ts: 1000 }]
+ */
+export function consolidateApiRequests(messages: ClineMessage[]): ClineMessage[] {
+	if (messages.length === 0) {
+		return []
+	}
+
+	if (messages.length === 1) {
+		return messages
+	}
+
+	let isMergeNecessary = false
+
+	for (const msg of messages) {
+		if (msg.type === "say" && (msg.say === "api_req_started" || msg.say === "api_req_finished")) {
+			isMergeNecessary = true
+			break
+		}
+	}
+
+	if (!isMergeNecessary) {
+		return messages
+	}
+
+	const result: ClineMessage[] = []
+	const startedIndices: number[] = []
+
+	for (const message of messages) {
+		if (message.type !== "say" || (message.say !== "api_req_started" && message.say !== "api_req_finished")) {
+			result.push(message)
+			continue
+		}
+
+		if (message.say === "api_req_started") {
+			// Add to result and track the index.
+			result.push(message)
+			startedIndices.push(result.length - 1)
+			continue
+		}
+
+		// Find the most recent api_req_started that hasn't been consolidated.
+		const startIndex = startedIndices.length > 0 ? startedIndices.pop() : undefined
+
+		if (startIndex !== undefined) {
+			const startMessage = result[startIndex]
+			if (!startMessage) continue
+
+			let startData = {}
+			let finishData = {}
+
+			try {
+				if (startMessage.text) {
+					startData = JSON.parse(startMessage.text)
+				}
+			} catch {
+				// Ignore JSON parse errors
+			}
+
+			try {
+				if (message.text) {
+					finishData = JSON.parse(message.text)
+				}
+			} catch {
+				// Ignore JSON parse errors
+			}
+
+			result[startIndex] = { ...startMessage, text: JSON.stringify({ ...startData, ...finishData }) }
+		}
+	}
+
+	return result
+}

+ 160 - 0
packages/core/src/message-utils/consolidateCommands.ts

@@ -0,0 +1,160 @@
+import type { ClineMessage } from "@roo-code/types"
+
+import { safeJsonParse } from "./safeJsonParse.js"
+
+export const COMMAND_OUTPUT_STRING = "Output:"
+
+/**
+ * Consolidates sequences of command and command_output messages in an array of ClineMessages.
+ * Also consolidates sequences of use_mcp_server and mcp_server_response messages.
+ *
+ * This function processes an array of ClineMessages objects, looking for sequences
+ * where a 'command' message is followed by one or more 'command_output' messages,
+ * or where a 'use_mcp_server' message is followed by one or more 'mcp_server_response' messages.
+ * When such a sequence is found, it consolidates them into a single message, merging
+ * their text contents.
+ *
+ * @param messages - An array of ClineMessage objects to process.
+ * @returns A new array of ClineMessage objects with command and MCP sequences consolidated.
+ *
+ * @example
+ * const messages: ClineMessage[] = [
+ *   { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 },
+ *   { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 },
+ *   { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 }
+ * ];
+ * const result = consolidateCommands(messages);
+ * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
+ */
+export function consolidateCommands(messages: ClineMessage[]): ClineMessage[] {
+	const consolidatedMessages = new Map<number, ClineMessage>()
+	const processedIndices = new Set<number>()
+
+	// Single pass through all messages
+	for (let i = 0; i < messages.length; i++) {
+		const msg = messages[i]
+		if (!msg) continue
+
+		// Handle MCP server requests
+		if (msg.type === "ask" && msg.ask === "use_mcp_server") {
+			// Look ahead for MCP responses
+			const responses: string[] = []
+			let j = i + 1
+
+			while (j < messages.length) {
+				const nextMsg = messages[j]
+				if (!nextMsg) {
+					j++
+					continue
+				}
+				if (nextMsg.say === "mcp_server_response") {
+					responses.push(nextMsg.text || "")
+					processedIndices.add(j)
+					j++
+				} else if (nextMsg.type === "ask" && nextMsg.ask === "use_mcp_server") {
+					// Stop if we encounter another MCP request
+					break
+				} else {
+					j++
+				}
+			}
+
+			if (responses.length > 0) {
+				// Parse the JSON from the message text
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				const jsonObj = safeJsonParse<any>(msg.text || "{}", {})
+
+				// Add the response to the JSON object
+				jsonObj.response = responses.join("\n")
+
+				// Stringify the updated JSON object
+				const consolidatedText = JSON.stringify(jsonObj)
+
+				consolidatedMessages.set(msg.ts, { ...msg, text: consolidatedText })
+			} else {
+				// If there's no response, just keep the original message
+				consolidatedMessages.set(msg.ts, { ...msg })
+			}
+		}
+		// Handle command sequences
+		else if (msg.type === "ask" && msg.ask === "command") {
+			let consolidatedText = msg.text || ""
+			let j = i + 1
+			let previous: { type: "ask" | "say"; text: string } | undefined
+			let lastProcessedIndex = i
+
+			while (j < messages.length) {
+				const currentMsg = messages[j]
+				if (!currentMsg) {
+					j++
+					continue
+				}
+				const { type, ask, say, text = "" } = currentMsg
+
+				if (type === "ask" && ask === "command") {
+					break // Stop if we encounter the next command.
+				}
+
+				if (ask === "command_output" || say === "command_output") {
+					if (!previous) {
+						consolidatedText += `\n${COMMAND_OUTPUT_STRING}`
+					}
+
+					const isDuplicate = previous && previous.type !== type && previous.text === text
+
+					if (text.length > 0 && !isDuplicate) {
+						// Add a newline before adding the text if there's already content
+						if (
+							previous &&
+							consolidatedText.length >
+								consolidatedText.indexOf(COMMAND_OUTPUT_STRING) + COMMAND_OUTPUT_STRING.length
+						) {
+							consolidatedText += "\n"
+						}
+						consolidatedText += text
+					}
+
+					previous = { type, text }
+					processedIndices.add(j)
+					lastProcessedIndex = j
+				}
+
+				j++
+			}
+
+			consolidatedMessages.set(msg.ts, { ...msg, text: consolidatedText })
+
+			// Only skip ahead if we actually processed command outputs
+			if (lastProcessedIndex > i) {
+				i = lastProcessedIndex
+			}
+		}
+	}
+
+	// Build final result: filter out processed messages and use consolidated versions
+	const result: ClineMessage[] = []
+	for (let i = 0; i < messages.length; i++) {
+		const msg = messages[i]
+		if (!msg) continue
+
+		// Skip messages that were processed as outputs/responses
+		if (processedIndices.has(i)) {
+			continue
+		}
+
+		// Skip command_output and mcp_server_response messages
+		if (msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response") {
+			continue
+		}
+
+		// Use consolidated version if available
+		const consolidatedMsg = consolidatedMessages.get(msg.ts)
+		if (consolidatedMsg) {
+			result.push(consolidatedMsg)
+		} else {
+			result.push(msg)
+		}
+	}
+
+	return result
+}

+ 157 - 0
packages/core/src/message-utils/consolidateTokenUsage.ts

@@ -0,0 +1,157 @@
+import type { TokenUsage, ToolUsage, ToolName, ClineMessage } from "@roo-code/types"
+
+export type ParsedApiReqStartedTextType = {
+	tokensIn: number
+	tokensOut: number
+	cacheWrites: number
+	cacheReads: number
+	cost?: number // Only present if consolidateApiRequests has been called
+	apiProtocol?: "anthropic" | "openai"
+}
+
+/**
+ * Consolidates token usage metrics from an array of ClineMessages.
+ *
+ * This function processes 'condense_context' messages and 'api_req_started' messages that have been
+ * consolidated with their corresponding 'api_req_finished' messages by the consolidateApiRequests function.
+ * It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
+ *
+ * @param messages - An array of ClineMessage objects to process.
+ * @returns A TokenUsage object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost, and contextTokens.
+ *
+ * @example
+ * const messages = [
+ *   { type: "say", say: "api_req_started", text: '{"request":"GET /api/data","tokensIn":10,"tokensOut":20,"cost":0.005}', ts: 1000 }
+ * ];
+ * const { totalTokensIn, totalTokensOut, totalCost } = consolidateTokenUsage(messages);
+ * // Result: { totalTokensIn: 10, totalTokensOut: 20, totalCost: 0.005 }
+ */
+export function consolidateTokenUsage(messages: ClineMessage[]): TokenUsage {
+	const result: TokenUsage = {
+		totalTokensIn: 0,
+		totalTokensOut: 0,
+		totalCacheWrites: undefined,
+		totalCacheReads: undefined,
+		totalCost: 0,
+		contextTokens: 0,
+	}
+
+	// Calculate running totals.
+	messages.forEach((message) => {
+		if (message.type === "say" && message.say === "api_req_started" && message.text) {
+			try {
+				const parsedText: ParsedApiReqStartedTextType = JSON.parse(message.text)
+				const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedText
+
+				if (typeof tokensIn === "number") {
+					result.totalTokensIn += tokensIn
+				}
+
+				if (typeof tokensOut === "number") {
+					result.totalTokensOut += tokensOut
+				}
+
+				if (typeof cacheWrites === "number") {
+					result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites
+				}
+
+				if (typeof cacheReads === "number") {
+					result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads
+				}
+
+				if (typeof cost === "number") {
+					result.totalCost += cost
+				}
+			} catch (error) {
+				console.error("Error parsing JSON:", error)
+			}
+		} else if (message.type === "say" && message.say === "condense_context") {
+			result.totalCost += message.contextCondense?.cost ?? 0
+		}
+	})
+
+	// Calculate context tokens, from the last API request started or condense
+	// context message.
+	result.contextTokens = 0
+
+	for (let i = messages.length - 1; i >= 0; i--) {
+		const message = messages[i]
+		if (!message) continue
+
+		if (message.type === "say" && message.say === "api_req_started" && message.text) {
+			try {
+				const parsedText: ParsedApiReqStartedTextType = JSON.parse(message.text)
+				const { tokensIn, tokensOut } = parsedText
+
+				// Since tokensIn now stores TOTAL input tokens (including cache tokens),
+				// we no longer need to add cacheWrites and cacheReads separately.
+				// This applies to both Anthropic and OpenAI protocols.
+				result.contextTokens = (tokensIn || 0) + (tokensOut || 0)
+			} catch {
+				// Ignore JSON parse errors
+				continue
+			}
+		} else if (message.type === "say" && message.say === "condense_context") {
+			result.contextTokens = message.contextCondense?.newContextTokens ?? 0
+		}
+		if (result.contextTokens) {
+			break
+		}
+	}
+
+	return result
+}
+
+/**
+ * Check if token usage has changed by comparing relevant properties.
+ * @param current - Current token usage data
+ * @param snapshot - Previous snapshot to compare against
+ * @returns true if any relevant property has changed or snapshot is undefined
+ */
+export function hasTokenUsageChanged(current: TokenUsage, snapshot?: TokenUsage): boolean {
+	if (!snapshot) {
+		return true
+	}
+
+	const keysToCompare: (keyof TokenUsage)[] = [
+		"totalTokensIn",
+		"totalTokensOut",
+		"totalCacheWrites",
+		"totalCacheReads",
+		"totalCost",
+		"contextTokens",
+	]
+
+	return keysToCompare.some((key) => current[key] !== snapshot[key])
+}
+
+/**
+ * Check if tool usage has changed by comparing attempts and failures.
+ * @param current - Current tool usage data
+ * @param snapshot - Previous snapshot to compare against (undefined treated as empty)
+ * @returns true if any tool's attempts/failures have changed between current and snapshot
+ */
+export function hasToolUsageChanged(current: ToolUsage, snapshot?: ToolUsage): boolean {
+	// Treat undefined snapshot as empty object for consistent comparison
+	const effectiveSnapshot = snapshot ?? {}
+
+	const currentKeys = Object.keys(current) as ToolName[]
+	const snapshotKeys = Object.keys(effectiveSnapshot) as ToolName[]
+
+	// Check if number of tools changed
+	if (currentKeys.length !== snapshotKeys.length) {
+		return true
+	}
+
+	// Check if any tool's stats changed
+	return currentKeys.some((key) => {
+		const currentTool = current[key]
+		const snapshotTool = effectiveSnapshot[key]
+
+		if (!snapshotTool || !currentTool) {
+			return true
+		}
+
+		return currentTool.attempts !== snapshotTool.attempts || currentTool.failures !== snapshotTool.failures
+	})
+}

+ 12 - 0
packages/core/src/message-utils/index.ts

@@ -0,0 +1,12 @@
+export {
+	type ParsedApiReqStartedTextType,
+	consolidateTokenUsage,
+	hasTokenUsageChanged,
+	hasToolUsageChanged,
+} from "./consolidateTokenUsage.js"
+
+export { consolidateApiRequests } from "./consolidateApiRequests.js"
+
+export { consolidateCommands, COMMAND_OUTPUT_STRING } from "./consolidateCommands.js"
+
+export { safeJsonParse } from "./safeJsonParse.js"

+ 2 - 2
src/shared/safeJsonParse.ts → packages/core/src/message-utils/safeJsonParse.ts

@@ -1,5 +1,5 @@
 /**
- * Safely parses JSON without crashing on invalid input
+ * Safely parses JSON without crashing on invalid input.
  *
  * @param jsonString The string to parse
  * @param defaultValue Value to return if parsing fails
@@ -13,7 +13,7 @@ export function safeJsonParse<T>(jsonString: string | null | undefined, defaultV
 	try {
 		return JSON.parse(jsonString) as T
 	} catch (error) {
-		// Log the error to the console for debugging
+		// Log the error to the console for debugging.
 		console.error("Error parsing JSON:", error)
 		return defaultValue
 	}

+ 22 - 0
packages/types/src/embedding.ts

@@ -0,0 +1,22 @@
+export type EmbedderProvider =
+	| "openai"
+	| "ollama"
+	| "openai-compatible"
+	| "gemini"
+	| "mistral"
+	| "vercel-ai-gateway"
+	| "bedrock"
+	| "openrouter" // Add other providers as needed.
+
+export interface EmbeddingModelProfile {
+	dimension: number
+	scoreThreshold?: number // Model-specific minimum score threshold for semantic search.
+	queryPrefix?: string // Optional prefix required by the model for queries.
+	// Add other model-specific properties if needed, e.g., context window size.
+}
+
+export type EmbeddingModelProfiles = {
+	[provider in EmbedderProvider]?: {
+		[modelId: string]: EmbeddingModelProfile
+	}
+}

+ 1 - 0
packages/types/src/index.ts

@@ -4,6 +4,7 @@ export * from "./codebase-index.js"
 export * from "./context-management.js"
 export * from "./cookie-consent.js"
 export * from "./custom-tool.js"
+export * from "./embedding.js"
 export * from "./events.js"
 export * from "./experiment.js"
 export * from "./followup.js"

+ 137 - 1
packages/types/src/vscode-extension-host.ts

@@ -507,7 +507,6 @@ export interface WebviewMessage {
 		| "requestClaudeCodeRateLimits"
 		| "refreshCustomTools"
 		| "requestModes"
-		| "switchMode"
 	text?: string
 	editedMessageContent?: string
 	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -642,3 +641,140 @@ export type WebViewMessagePayload =
 	| InstallMarketplaceItemWithParametersPayload
 	| UpdateTodoListPayload
 	| EditQueuedMessagePayload
+
+export interface IndexingStatus {
+	systemStatus: string
+	message?: string
+	processedItems: number
+	totalItems: number
+	currentItemUnit?: string
+	workspacePath?: string
+}
+
+export interface IndexingStatusUpdateMessage {
+	type: "indexingStatusUpdate"
+	values: IndexingStatus
+}
+
+export interface LanguageModelChatSelector {
+	vendor?: string
+	family?: string
+	version?: string
+	id?: string
+}
+
+export interface ClineSayTool {
+	tool:
+		| "editedExistingFile"
+		| "appliedDiff"
+		| "newFileCreated"
+		| "codebaseSearch"
+		| "readFile"
+		| "fetchInstructions"
+		| "listFilesTopLevel"
+		| "listFilesRecursive"
+		| "searchFiles"
+		| "switchMode"
+		| "newTask"
+		| "finishTask"
+		| "generateImage"
+		| "imageGenerated"
+		| "runSlashCommand"
+		| "updateTodoList"
+	path?: string
+	diff?: string
+	content?: string
+	// Unified diff statistics computed by the extension
+	diffStats?: { added: number; removed: number }
+	regex?: string
+	filePattern?: string
+	mode?: string
+	reason?: string
+	isOutsideWorkspace?: boolean
+	isProtected?: boolean
+	additionalFileCount?: number // Number of additional files in the same read_file request
+	lineNumber?: number
+	query?: string
+	batchFiles?: Array<{
+		path: string
+		lineSnippet: string
+		isOutsideWorkspace?: boolean
+		key: string
+		content?: string
+	}>
+	batchDiffs?: Array<{
+		path: string
+		changeCount: number
+		key: string
+		content: string
+		// Per-file unified diff statistics computed by the extension
+		diffStats?: { added: number; removed: number }
+		diffs?: Array<{
+			content: string
+			startLine?: number
+		}>
+	}>
+	question?: string
+	imageData?: string // Base64 encoded image data for generated images
+	// Properties for runSlashCommand tool
+	command?: string
+	args?: string
+	source?: string
+	description?: string
+}
+
+// Must keep in sync with system prompt.
+export const browserActions = [
+	"launch",
+	"click",
+	"hover",
+	"type",
+	"press",
+	"scroll_down",
+	"scroll_up",
+	"resize",
+	"close",
+	"screenshot",
+] as const
+
+export type BrowserAction = (typeof browserActions)[number]
+
+export interface ClineSayBrowserAction {
+	action: BrowserAction
+	coordinate?: string
+	size?: string
+	text?: string
+	executedCoordinate?: string
+}
+
+export type BrowserActionResult = {
+	screenshot?: string
+	logs?: string
+	currentUrl?: string
+	currentMousePosition?: string
+	viewportWidth?: number
+	viewportHeight?: number
+}
+
+export interface ClineAskUseMcpServer {
+	serverName: string
+	type: "use_mcp_tool" | "access_mcp_resource"
+	toolName?: string
+	arguments?: string
+	uri?: string
+	response?: string
+}
+
+export interface ClineApiReqInfo {
+	request?: string
+	tokensIn?: number
+	tokensOut?: number
+	cacheWrites?: number
+	cacheReads?: number
+	cost?: number
+	cancelReason?: ClineApiReqCancelReason
+	streamingFailedMessage?: string
+	apiProtocol?: "anthropic" | "openai"
+}
+
+export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

+ 8 - 0
packages/vscode-shim/src/api/create-vscode-api-mock.ts

@@ -68,6 +68,13 @@ export interface VSCodeAPIMockOptions {
 	 * Defaults to the directory containing this module.
 	 */
 	appRoot?: string
+
+	/**
+	 * Custom storage directory for persistent state.
+	 * Defaults to ~/.vscode-mock.
+	 * Set to a temp directory for ephemeral/no-persist mode.
+	 */
+	storageDir?: string
 }
 
 /**
@@ -82,6 +89,7 @@ export function createVSCodeAPIMock(
 	const context = new ExtensionContextImpl({
 		extensionPath: extensionRootPath,
 		workspacePath: workspacePath,
+		storageDir: options?.storageDir,
 	})
 	const workspace = new WorkspaceAPI(workspacePath, context)
 	const window = new WindowAPI()

+ 1 - 1
src/api/providers/anthropic-vertex.ts

@@ -11,9 +11,9 @@ import {
 	TOOL_PROTOCOL,
 	VERTEX_1M_CONTEXT_MODEL_IDS,
 } from "@roo-code/types"
+import { safeJsonParse } from "@roo-code/core"
 
 import { ApiHandlerOptions } from "../../shared/api"
-import { safeJsonParse } from "../../shared/safeJsonParse"
 
 import { ApiStream } from "../transform/stream"
 import { addCacheBreakpoints } from "../transform/caching/vertex"

+ 1 - 2
src/api/providers/gemini.ts

@@ -16,16 +16,15 @@ import {
 	geminiModels,
 	ApiProviderError,
 } from "@roo-code/types"
+import { safeJsonParse } from "@roo-code/core"
 import { TelemetryService } from "@roo-code/telemetry"
 
 import type { ApiHandlerOptions } from "../../shared/api"
-import { safeJsonParse } from "../../shared/safeJsonParse"
 
 import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
 import { t } from "i18next"
 import type { ApiStream, GroundingSource } from "../transform/stream"
 import { getModelParams } from "../transform/model-params"
-import { handleProviderError } from "./utils/error-handler"
 
 import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
 import { BaseProvider } from "./base-provider"

+ 1 - 1
src/core/auto-approval/index.ts

@@ -1,12 +1,12 @@
 import {
 	type ClineAsk,
+	type ClineSayTool,
 	type McpServerUse,
 	type FollowUpData,
 	type ExtensionState,
 	isNonBlockingAsk,
 } from "@roo-code/types"
 
-import type { ClineSayTool } from "../../shared/ExtensionMessage"
 import { ClineAskResponse } from "../../shared/WebviewMessage"
 
 import { isWriteToolAction, isReadOnlyToolAction } from "./tools"

+ 1 - 1
src/core/auto-approval/tools.ts

@@ -1,4 +1,4 @@
-import type { ClineSayTool } from "../../shared/ExtensionMessage"
+import type { ClineSayTool } from "@roo-code/types"
 
 export function isWriteToolAction(tool: ClineSayTool): boolean {
 	return ["editedExistingFile", "appliedDiff", "newFileCreated", "generateImage"].includes(tool.tool)

+ 1 - 1
src/core/checkpoints/index.ts

@@ -1,6 +1,7 @@
 import pWaitFor from "p-wait-for"
 import * as vscode from "vscode"
 
+import type { ClineApiReqInfo } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
 
 import { Task } from "../task/Task"
@@ -9,7 +10,6 @@ import { getWorkspacePath } from "../../utils/path"
 import { checkGitInstalled } from "../../utils/git"
 import { t } from "../../i18n"
 
-import { ClineApiReqInfo } from "../../shared/ExtensionMessage"
 import { getApiMetrics } from "../../shared/getApiMetrics"
 
 import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"

+ 2 - 1
src/core/task/Task.ts

@@ -33,6 +33,8 @@ import {
 	type CreateTaskOptions,
 	type ModelInfo,
 	type ToolProtocol,
+	type ClineApiReqCancelReason,
+	type ClineApiReqInfo,
 	RooCodeEventName,
 	TelemetryEventName,
 	TaskStatus,
@@ -65,7 +67,6 @@ import { findLastIndex } from "../../shared/array"
 import { combineApiRequests } from "../../shared/combineApiRequests"
 import { combineCommandSequences } from "../../shared/combineCommandSequences"
 import { t } from "../../i18n"
-import { ClineApiReqCancelReason, ClineApiReqInfo } from "../../shared/ExtensionMessage"
 import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../shared/getApiMetrics"
 import { ClineAskResponse } from "../../shared/WebviewMessage"
 import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"

+ 3 - 3
src/core/tools/ApplyDiffTool.ts

@@ -1,10 +1,9 @@
 import path from "path"
 import fs from "fs/promises"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { getReadablePath } from "../../utils/path"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
@@ -13,9 +12,10 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { unescapeHtmlEntities } from "../../utils/text-normalization"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface ApplyDiffParams {
 	path: string
 	diff: string

+ 2 - 2
src/core/tools/ApplyPatchTool.ts

@@ -1,14 +1,14 @@
 import fs from "fs/promises"
 import path from "path"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
+
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath } from "../../utils/fs"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats"
 import { BaseTool, ToolCallbacks } from "./BaseTool"

+ 2 - 1
src/core/tools/AskFollowupQuestionTool.ts

@@ -1,9 +1,10 @@
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
 import { parseXml } from "../../utils/xml"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface Suggestion {
 	text: string
 	mode?: string

+ 2 - 1
src/core/tools/AttemptCompletionTool.ts

@@ -6,10 +6,11 @@ import { TelemetryService } from "@roo-code/telemetry"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
 import { Package } from "../../shared/package"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 import { t } from "../../i18n"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface AttemptCompletionParams {
 	result: string
 	command?: string

+ 2 - 1
src/core/tools/BaseTool.ts

@@ -1,3 +1,5 @@
+import type { ToolName, ToolProtocol } from "@roo-code/types"
+
 import { Task } from "../task/Task"
 import type {
 	ToolUse,
@@ -7,7 +9,6 @@ import type {
 	AskApproval,
 	NativeToolArgs,
 } from "../../shared/tools"
-import type { ToolName, ToolProtocol } from "@roo-code/types"
 
 /**
  * Callbacks passed to tool execution

+ 5 - 7
src/core/tools/BrowserActionTool.ts

@@ -1,13 +1,11 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+
+import { BrowserAction, BrowserActionResult, browserActions, ClineSayBrowserAction } from "@roo-code/types"
+
 import { Task } from "../task/Task"
 import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
-import {
-	BrowserAction,
-	BrowserActionResult,
-	browserActions,
-	ClineSayBrowserAction,
-} from "../../shared/ExtensionMessage"
 import { formatResponse } from "../prompts/responses"
-import { Anthropic } from "@anthropic-ai/sdk"
+
 import { scaleCoordinate } from "../../shared/browserUtils"
 
 export async function browserActionTool(

+ 2 - 1
src/core/tools/CodebaseSearchTool.ts

@@ -6,9 +6,10 @@ import { CodeIndexManager } from "../../services/code-index/manager"
 import { getWorkspacePath } from "../../utils/path"
 import { formatResponse } from "../prompts/responses"
 import { VectorStoreSearchResult } from "../../services/code-index/interfaces"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface CodebaseSearchParams {
 	query: string
 	path?: string

+ 4 - 3
src/core/tools/EditFileTool.ts

@@ -1,19 +1,20 @@
 import fs from "fs/promises"
 import path from "path"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
+
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath } from "../../utils/fs"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface EditFileParams {
 	file_path: string
 	old_string: string

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

@@ -1,10 +1,12 @@
+import { type ClineSayTool } from "@roo-code/types"
+
 import { Task } from "../task/Task"
 import { fetchInstructions } from "../prompts/instructions/instructions"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { formatResponse } from "../prompts/responses"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface FetchInstructionsParams {
 	task: string
 }

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

@@ -1,14 +1,16 @@
 import * as path from "path"
 
+import { type ClineSayTool } from "@roo-code/types"
+
 import { Task } from "../task/Task"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { formatResponse } from "../prompts/responses"
 import { listFiles } from "../../services/glob/list-files"
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface ListFilesParams {
 	path: string
 	recursive?: boolean

+ 1 - 3
src/core/tools/MultiApplyDiffTool.ts

@@ -1,10 +1,9 @@
 import path from "path"
 import fs from "fs/promises"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types"
 import { TelemetryService } from "@roo-code/telemetry"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { getReadablePath } from "../../utils/path"
 import { Task } from "../task/Task"
 import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools"
@@ -16,7 +15,6 @@ import { parseXmlForDiff } from "../../utils/xml"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { applyDiffTool as applyDiffToolClass } from "./ApplyDiffTool"
 import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
-import { isNativeProtocol } from "@roo-code/types"
 import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
 
 interface DiffOperation {

+ 4 - 3
src/core/tools/ReadFileTool.ts

@@ -1,11 +1,11 @@
 import path from "path"
 import * as fs from "fs/promises"
 import { isBinaryFile } from "isbinaryfile"
+
 import type { FileEntry, LineRange } from "@roo-code/types"
-import { isNativeProtocol, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
+import { type ClineSayTool, isNativeProtocol, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
 
 import { Task } from "../task/Task"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { formatResponse } from "../prompts/responses"
 import { getModelMaxOutputTokens } from "../../shared/api"
 import { t } from "../../i18n"
@@ -18,6 +18,8 @@ import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "
 import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
 import { parseXml } from "../../utils/xml"
 import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
+import type { ToolUse } from "../../shared/tools"
+
 import {
 	DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
 	DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
@@ -29,7 +31,6 @@ import {
 import { FILE_READ_BUDGET_PERCENT, readFileWithTokenBudget } from "./helpers/fileTokenBudget"
 import { truncateDefinitionsToLineLimit } from "./helpers/truncateDefinitions"
 import { BaseTool, ToolCallbacks } from "./BaseTool"
-import type { ToolUse } from "../../shared/tools"
 
 interface FileResult {
 	path: string

+ 4 - 3
src/core/tools/SearchAndReplaceTool.ts

@@ -1,19 +1,20 @@
 import fs from "fs/promises"
 import path from "path"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
+
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath } from "../../utils/fs"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface SearchReplaceOperation {
 	search: string
 	replace: string

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

@@ -1,13 +1,15 @@
 import path from "path"
 
+import { type ClineSayTool } from "@roo-code/types"
+
 import { Task } from "../task/Task"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { regexSearchFiles } from "../../services/ripgrep"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface SearchFilesParams {
 	path: string
 	regex: string

+ 4 - 3
src/core/tools/SearchReplaceTool.ts

@@ -1,19 +1,20 @@
 import fs from "fs/promises"
 import path from "path"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
+
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath } from "../../utils/fs"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface SearchReplaceParams {
 	file_path: string
 	old_string: string

+ 4 - 3
src/core/tools/UseMcpToolTool.ts

@@ -1,11 +1,12 @@
+import type { ClineAskUseMcpServer, McpExecutionStatus } from "@roo-code/types"
+
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
-import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage"
-import { McpExecutionStatus } from "@roo-code/types"
 import { t } from "../../i18n"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface UseMcpToolParams {
 	server_name: string
 	tool_name: string

+ 4 - 4
src/core/tools/WriteToFileTool.ts

@@ -1,10 +1,10 @@
 import path from "path"
 import delay from "delay"
-import * as vscode from "vscode"
 import fs from "fs/promises"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
+
 import { Task } from "../task/Task"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { formatResponse } from "../prompts/responses"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath, createDirectoriesForFile } from "../../utils/fs"
@@ -12,12 +12,12 @@ import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/mi
 import { getReadablePath } from "../../utils/path"
 import { isPathOutsideWorkspace } from "../../utils/pathUtils"
 import { unescapeHtmlEntities } from "../../utils/text-normalization"
-import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
 import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
 import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
-import { BaseTool, ToolCallbacks } from "./BaseTool"
 import type { ToolUse } from "../../shared/tools"
 
+import { BaseTool, ToolCallbacks } from "./BaseTool"
+
 interface WriteToFileParams {
 	path: string
 	content: string

+ 1 - 3
src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts

@@ -1,6 +1,4 @@
-// Test screenshot action functionality in browser actions
-import { describe, it, expect } from "vitest"
-import { browserActions } from "../../../shared/ExtensionMessage"
+import { browserActions } from "@roo-code/types"
 
 describe("Browser Action Screenshot", () => {
 	describe("browserActions array", () => {

+ 3 - 1
src/core/tools/accessMcpResourceTool.ts

@@ -1,7 +1,9 @@
-import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage"
+import type { ClineAskUseMcpServer } from "@roo-code/types"
+
 import type { ToolUse } from "../../shared/tools"
 import { Task } from "../task/Task"
 import { formatResponse } from "../prompts/responses"
+
 import { BaseTool, ToolCallbacks } from "./BaseTool"
 
 interface AccessMcpResourceParams {

+ 2 - 2
src/integrations/editor/DiffViewProvider.ts

@@ -6,13 +6,13 @@ import stripBom from "strip-bom"
 import { XMLBuilder } from "fast-xml-parser"
 import delay from "delay"
 
+import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types"
+
 import { createDirectoriesForFile } from "../../utils/fs"
 import { arePathsEqual, getReadablePath } from "../../utils/path"
 import { formatResponse } from "../../core/prompts/responses"
 import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
 import { Task } from "../../core/task/Task"
-import { DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types"
 import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
 
 import { DecorationController } from "./DecorationController"

+ 4 - 1
src/services/browser/BrowserSession.ts

@@ -6,8 +6,11 @@ import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect, KeyInp
 import PCR from "puppeteer-chromium-resolver"
 import pWaitFor from "p-wait-for"
 import delay from "delay"
+
+import { type BrowserActionResult } from "@roo-code/types"
+
 import { fileExistsAtPath } from "../../utils/fs"
-import { BrowserActionResult } from "../../shared/ExtensionMessage"
+
 import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery"
 
 // Timeout constants

+ 13 - 7
src/services/code-index/service-factory.ts

@@ -1,4 +1,17 @@
 import * as vscode from "vscode"
+import { Ignore } from "ignore"
+
+import type { EmbedderProvider } from "@roo-code/types"
+import { TelemetryService } from "@roo-code/telemetry"
+import { TelemetryEventName } from "@roo-code/types"
+
+import { t } from "../../i18n"
+
+import { getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
+import { Package } from "../../shared/package"
+
+import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
+
 import { OpenAiEmbedder } from "./embedders/openai"
 import { CodeIndexOllamaEmbedder } from "./embedders/ollama"
 import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible"
@@ -7,18 +20,11 @@ import { MistralEmbedder } from "./embedders/mistral"
 import { VercelAiGatewayEmbedder } from "./embedders/vercel-ai-gateway"
 import { BedrockEmbedder } from "./embedders/bedrock"
 import { OpenRouterEmbedder } from "./embedders/openrouter"
-import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
 import { QdrantVectorStore } from "./vector-store/qdrant-client"
 import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
 import { ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
 import { CodeIndexConfigManager } from "./config-manager"
 import { CacheManager } from "./cache-manager"
-import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
-import { Ignore } from "ignore"
-import { t } from "../../i18n"
-import { TelemetryService } from "@roo-code/telemetry"
-import { TelemetryEventName } from "@roo-code/types"
-import { Package } from "../../shared/package"
 import { BATCH_SEGMENT_THRESHOLD } from "./constants"
 
 /**

+ 0 - 136
src/shared/ExtensionMessage.ts

@@ -1,136 +0,0 @@
-export interface IndexingStatus {
-	systemStatus: string
-	message?: string
-	processedItems: number
-	totalItems: number
-	currentItemUnit?: string
-	workspacePath?: string
-}
-
-export interface IndexingStatusUpdateMessage {
-	type: "indexingStatusUpdate"
-	values: IndexingStatus
-}
-
-export interface LanguageModelChatSelector {
-	vendor?: string
-	family?: string
-	version?: string
-	id?: string
-}
-
-export interface ClineSayTool {
-	tool:
-		| "editedExistingFile"
-		| "appliedDiff"
-		| "newFileCreated"
-		| "codebaseSearch"
-		| "readFile"
-		| "fetchInstructions"
-		| "listFilesTopLevel"
-		| "listFilesRecursive"
-		| "searchFiles"
-		| "switchMode"
-		| "newTask"
-		| "finishTask"
-		| "generateImage"
-		| "imageGenerated"
-		| "runSlashCommand"
-		| "updateTodoList"
-	path?: string
-	diff?: string
-	content?: string
-	// Unified diff statistics computed by the extension
-	diffStats?: { added: number; removed: number }
-	regex?: string
-	filePattern?: string
-	mode?: string
-	reason?: string
-	isOutsideWorkspace?: boolean
-	isProtected?: boolean
-	additionalFileCount?: number // Number of additional files in the same read_file request
-	lineNumber?: number
-	query?: string
-	batchFiles?: Array<{
-		path: string
-		lineSnippet: string
-		isOutsideWorkspace?: boolean
-		key: string
-		content?: string
-	}>
-	batchDiffs?: Array<{
-		path: string
-		changeCount: number
-		key: string
-		content: string
-		// Per-file unified diff statistics computed by the extension
-		diffStats?: { added: number; removed: number }
-		diffs?: Array<{
-			content: string
-			startLine?: number
-		}>
-	}>
-	question?: string
-	imageData?: string // Base64 encoded image data for generated images
-	// Properties for runSlashCommand tool
-	command?: string
-	args?: string
-	source?: string
-	description?: string
-}
-
-// Must keep in sync with system prompt.
-export const browserActions = [
-	"launch",
-	"click",
-	"hover",
-	"type",
-	"press",
-	"scroll_down",
-	"scroll_up",
-	"resize",
-	"close",
-	"screenshot",
-] as const
-
-export type BrowserAction = (typeof browserActions)[number]
-
-export interface ClineSayBrowserAction {
-	action: BrowserAction
-	coordinate?: string
-	size?: string
-	text?: string
-	executedCoordinate?: string
-}
-
-export type BrowserActionResult = {
-	screenshot?: string
-	logs?: string
-	currentUrl?: string
-	currentMousePosition?: string
-	viewportWidth?: number
-	viewportHeight?: number
-}
-
-export interface ClineAskUseMcpServer {
-	serverName: string
-	type: "use_mcp_tool" | "access_mcp_resource"
-	toolName?: string
-	arguments?: string
-	uri?: string
-	response?: string
-}
-
-export interface ClineApiReqInfo {
-	request?: string
-	tokensIn?: number
-	tokensOut?: number
-	cacheWrites?: number
-	cacheReads?: number
-	cost?: number
-	cancelReason?: ClineApiReqCancelReason
-	streamingFailedMessage?: string
-	apiProtocol?: "anthropic" | "openai"
-}
-
-export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

+ 2 - 83
src/shared/combineApiRequests.ts

@@ -1,84 +1,3 @@
-import type { ClineMessage } from "@roo-code/types"
+import { consolidateApiRequests as combineApiRequests } from "@roo-code/core/browser"
 
-/**
- * Combines API request start and finish messages in an array of ClineMessages.
- *
- * This function looks for pairs of 'api_req_started' and 'api_req_finished' messages.
- * When it finds a pair, it combines them into a single 'api_req_combined' message.
- * The JSON data in the text fields of both messages are merged.
- *
- * @param messages - An array of ClineMessage objects to process.
- * @returns A new array of ClineMessage objects with API requests combined.
- *
- * @example
- * const messages = [
- *   { type: "say", say: "api_req_started", text: '{"request":"GET /api/data"}', ts: 1000 },
- *   { type: "say", say: "api_req_finished", text: '{"cost":0.005}', ts: 1001 }
- * ];
- * const result = combineApiRequests(messages);
- * // Result: [{ type: "say", say: "api_req_started", text: '{"request":"GET /api/data","cost":0.005}', ts: 1000 }]
- */
-export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
-	if (messages.length === 0) {
-		return []
-	}
-
-	if (messages.length === 1) {
-		return messages
-	}
-
-	let isMergeNecessary = false
-
-	for (const msg of messages) {
-		if (msg.type === "say" && (msg.say === "api_req_started" || msg.say === "api_req_finished")) {
-			isMergeNecessary = true
-			break
-		}
-	}
-
-	if (!isMergeNecessary) {
-		return messages
-	}
-
-	const result: ClineMessage[] = []
-	const startedIndices: number[] = []
-
-	for (const message of messages) {
-		if (message.type !== "say" || (message.say !== "api_req_started" && message.say !== "api_req_finished")) {
-			result.push(message)
-			continue
-		}
-
-		if (message.say === "api_req_started") {
-			// Add to result and track the index.
-			result.push(message)
-			startedIndices.push(result.length - 1)
-			continue
-		}
-
-		// Find the most recent api_req_started that hasn't been combined.
-		const startIndex = startedIndices.length > 0 ? startedIndices.pop() : undefined
-
-		if (startIndex !== undefined) {
-			const startMessage = result[startIndex]
-			let startData = {}
-			let finishData = {}
-
-			try {
-				if (startMessage.text) {
-					startData = JSON.parse(startMessage.text)
-				}
-			} catch (e) {}
-
-			try {
-				if (message.text) {
-					finishData = JSON.parse(message.text)
-				}
-			} catch (e) {}
-
-			result[startIndex] = { ...startMessage, text: JSON.stringify({ ...startData, ...finishData }) }
-		}
-	}
-
-	return result
-}
+export { combineApiRequests }

+ 2 - 145
src/shared/combineCommandSequences.ts

@@ -1,146 +1,3 @@
-import type { ClineMessage } from "@roo-code/types"
+import { consolidateCommands as combineCommandSequences, COMMAND_OUTPUT_STRING } from "@roo-code/core/browser"
 
-import { safeJsonParse } from "./safeJsonParse"
-
-export const COMMAND_OUTPUT_STRING = "Output:"
-
-/**
- * Combines sequences of command and command_output messages in an array of ClineMessages.
- * Also combines sequences of use_mcp_server and mcp_server_response messages.
- *
- * This function processes an array of ClineMessages objects, looking for sequences
- * where a 'command' message is followed by one or more 'command_output' messages,
- * or where a 'use_mcp_server' message is followed by one or more 'mcp_server_response' messages.
- * When such a sequence is found, it combines them into a single message, merging
- * their text contents.
- *
- * @param messages - An array of ClineMessage objects to process.
- * @returns A new array of ClineMessage objects with command and MCP sequences combined.
- *
- * @example
- * const messages: ClineMessage[] = [
- *   { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 },
- *   { type: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 },
- *   { type: 'ask', ask: 'command_output', text: 'file2.txt', ts: 1625097602000 }
- * ];
- * const result = simpleCombineCommandSequences(messages);
- * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
- */
-export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] {
-	const combinedMessages = new Map<number, ClineMessage>()
-	const processedIndices = new Set<number>()
-
-	// Single pass through all messages
-	for (let i = 0; i < messages.length; i++) {
-		const msg = messages[i]
-
-		// Handle MCP server requests
-		if (msg.type === "ask" && msg.ask === "use_mcp_server") {
-			// Look ahead for MCP responses
-			let responses: string[] = []
-			let j = i + 1
-
-			while (j < messages.length) {
-				if (messages[j].say === "mcp_server_response") {
-					responses.push(messages[j].text || "")
-					processedIndices.add(j)
-					j++
-				} else if (messages[j].type === "ask" && messages[j].ask === "use_mcp_server") {
-					// Stop if we encounter another MCP request
-					break
-				} else {
-					j++
-				}
-			}
-
-			if (responses.length > 0) {
-				// Parse the JSON from the message text
-				const jsonObj = safeJsonParse<any>(msg.text || "{}", {})
-
-				// Add the response to the JSON object
-				jsonObj.response = responses.join("\n")
-
-				// Stringify the updated JSON object
-				const combinedText = JSON.stringify(jsonObj)
-
-				combinedMessages.set(msg.ts, { ...msg, text: combinedText })
-			} else {
-				// If there's no response, just keep the original message
-				combinedMessages.set(msg.ts, { ...msg })
-			}
-		}
-		// Handle command sequences
-		else if (msg.type === "ask" && msg.ask === "command") {
-			let combinedText = msg.text || ""
-			let j = i + 1
-			let previous: { type: "ask" | "say"; text: string } | undefined
-			let lastProcessedIndex = i
-
-			while (j < messages.length) {
-				const { type, ask, say, text = "" } = messages[j]
-
-				if (type === "ask" && ask === "command") {
-					break // Stop if we encounter the next command.
-				}
-
-				if (ask === "command_output" || say === "command_output") {
-					if (!previous) {
-						combinedText += `\n${COMMAND_OUTPUT_STRING}`
-					}
-
-					const isDuplicate = previous && previous.type !== type && previous.text === text
-
-					if (text.length > 0 && !isDuplicate) {
-						// Add a newline before adding the text if there's already content
-						if (
-							previous &&
-							combinedText.length >
-								combinedText.indexOf(COMMAND_OUTPUT_STRING) + COMMAND_OUTPUT_STRING.length
-						) {
-							combinedText += "\n"
-						}
-						combinedText += text
-					}
-
-					previous = { type, text }
-					processedIndices.add(j)
-					lastProcessedIndex = j
-				}
-
-				j++
-			}
-
-			combinedMessages.set(msg.ts, { ...msg, text: combinedText })
-
-			// Only skip ahead if we actually processed command outputs
-			if (lastProcessedIndex > i) {
-				i = lastProcessedIndex
-			}
-		}
-	}
-
-	// Build final result: filter out processed messages and use combined versions
-	const result: ClineMessage[] = []
-	for (let i = 0; i < messages.length; i++) {
-		const msg = messages[i]
-
-		// Skip messages that were processed as outputs/responses
-		if (processedIndices.has(i)) {
-			continue
-		}
-
-		// Skip command_output and mcp_server_response messages
-		if (msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response") {
-			continue
-		}
-
-		// Use combined version if available
-		if (combinedMessages.has(msg.ts)) {
-			result.push(combinedMessages.get(msg.ts)!)
-		} else {
-			result.push(msg)
-		}
-	}
-
-	return result
-}
+export { combineCommandSequences, COMMAND_OUTPUT_STRING }

+ 1 - 0
src/shared/core.ts

@@ -0,0 +1 @@
+export * from "@roo-code/core/browser"

+ 1 - 22
src/shared/embeddingModels.ts

@@ -2,28 +2,7 @@
  * Defines profiles for different embedding models, including their dimensions.
  */
 
-export type EmbedderProvider =
-	| "openai"
-	| "ollama"
-	| "openai-compatible"
-	| "gemini"
-	| "mistral"
-	| "vercel-ai-gateway"
-	| "bedrock"
-	| "openrouter" // Add other providers as needed
-
-export interface EmbeddingModelProfile {
-	dimension: number
-	scoreThreshold?: number // Model-specific minimum score threshold for semantic search
-	queryPrefix?: string // Optional prefix required by the model for queries
-	// Add other model-specific properties if needed, e.g., context window size
-}
-
-export type EmbeddingModelProfiles = {
-	[provider in EmbedderProvider]?: {
-		[modelId: string]: EmbeddingModelProfile
-	}
-}
+import type { EmbedderProvider, EmbeddingModelProfiles } from "@roo-code/types"
 
 // Example profiles - expand this list as needed
 export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = {

+ 8 - 156
src/shared/getApiMetrics.ts

@@ -1,156 +1,8 @@
-import type { TokenUsage, ToolUsage, ToolName, ClineMessage } from "@roo-code/types"
-
-export type ParsedApiReqStartedTextType = {
-	tokensIn: number
-	tokensOut: number
-	cacheWrites: number
-	cacheReads: number
-	cost?: number // Only present if combineApiRequests has been called
-	apiProtocol?: "anthropic" | "openai"
-}
-
-/**
- * Calculates API metrics from an array of ClineMessages.
- *
- * This function processes 'condense_context' messages and 'api_req_started' messages that have been
- * combined with their corresponding 'api_req_finished' messages by the combineApiRequests function.
- * It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
- *
- * @param messages - An array of ClineMessage objects to process.
- * @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost, and contextTokens.
- *
- * @example
- * const messages = [
- *   { type: "say", say: "api_req_started", text: '{"request":"GET /api/data","tokensIn":10,"tokensOut":20,"cost":0.005}', ts: 1000 }
- * ];
- * const { totalTokensIn, totalTokensOut, totalCost } = getApiMetrics(messages);
- * // Result: { totalTokensIn: 10, totalTokensOut: 20, totalCost: 0.005 }
- */
-export function getApiMetrics(messages: ClineMessage[]) {
-	const result: TokenUsage = {
-		totalTokensIn: 0,
-		totalTokensOut: 0,
-		totalCacheWrites: undefined,
-		totalCacheReads: undefined,
-		totalCost: 0,
-		contextTokens: 0,
-	}
-
-	// Calculate running totals.
-	messages.forEach((message) => {
-		if (message.type === "say" && message.say === "api_req_started" && message.text) {
-			try {
-				const parsedText: ParsedApiReqStartedTextType = JSON.parse(message.text)
-				const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedText
-
-				if (typeof tokensIn === "number") {
-					result.totalTokensIn += tokensIn
-				}
-
-				if (typeof tokensOut === "number") {
-					result.totalTokensOut += tokensOut
-				}
-
-				if (typeof cacheWrites === "number") {
-					result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites
-				}
-
-				if (typeof cacheReads === "number") {
-					result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads
-				}
-
-				if (typeof cost === "number") {
-					result.totalCost += cost
-				}
-			} catch (error) {
-				console.error("Error parsing JSON:", error)
-			}
-		} else if (message.type === "say" && message.say === "condense_context") {
-			result.totalCost += message.contextCondense?.cost ?? 0
-		}
-	})
-
-	// Calculate context tokens, from the last API request started or condense
-	// context message.
-	result.contextTokens = 0
-
-	for (let i = messages.length - 1; i >= 0; i--) {
-		const message = messages[i]
-
-		if (message.type === "say" && message.say === "api_req_started" && message.text) {
-			try {
-				const parsedText: ParsedApiReqStartedTextType = JSON.parse(message.text)
-				const { tokensIn, tokensOut } = parsedText
-
-				// Since tokensIn now stores TOTAL input tokens (including cache tokens),
-				// we no longer need to add cacheWrites and cacheReads separately.
-				// This applies to both Anthropic and OpenAI protocols.
-				result.contextTokens = (tokensIn || 0) + (tokensOut || 0)
-			} catch (error) {
-				console.error("Error parsing JSON:", error)
-				continue
-			}
-		} else if (message.type === "say" && message.say === "condense_context") {
-			result.contextTokens = message.contextCondense?.newContextTokens ?? 0
-		}
-		if (result.contextTokens) {
-			break
-		}
-	}
-
-	return result
-}
-
-/**
- * Check if token usage has changed by comparing relevant properties.
- * @param current - Current token usage data
- * @param snapshot - Previous snapshot to compare against
- * @returns true if any relevant property has changed or snapshot is undefined
- */
-export function hasTokenUsageChanged(current: TokenUsage, snapshot?: TokenUsage): boolean {
-	if (!snapshot) {
-		return true
-	}
-
-	const keysToCompare: (keyof TokenUsage)[] = [
-		"totalTokensIn",
-		"totalTokensOut",
-		"totalCacheWrites",
-		"totalCacheReads",
-		"totalCost",
-		"contextTokens",
-	]
-
-	return keysToCompare.some((key) => current[key] !== snapshot[key])
-}
-
-/**
- * Check if tool usage has changed by comparing attempts and failures.
- * @param current - Current tool usage data
- * @param snapshot - Previous snapshot to compare against (undefined treated as empty)
- * @returns true if any tool's attempts/failures have changed between current and snapshot
- */
-export function hasToolUsageChanged(current: ToolUsage, snapshot?: ToolUsage): boolean {
-	// Treat undefined snapshot as empty object for consistent comparison
-	const effectiveSnapshot = snapshot ?? {}
-
-	const currentKeys = Object.keys(current) as ToolName[]
-	const snapshotKeys = Object.keys(effectiveSnapshot) as ToolName[]
-
-	// Check if number of tools changed
-	if (currentKeys.length !== snapshotKeys.length) {
-		return true
-	}
-
-	// Check if any tool's stats changed
-	return currentKeys.some((key) => {
-		const currentTool = current[key]
-		const snapshotTool = effectiveSnapshot[key]
-
-		if (!snapshotTool || !currentTool) {
-			return true
-		}
-
-		return currentTool.attempts !== snapshotTool.attempts || currentTool.failures !== snapshotTool.failures
-	})
-}
+import {
+	type ParsedApiReqStartedTextType,
+	consolidateTokenUsage as getApiMetrics,
+	hasTokenUsageChanged,
+	hasToolUsageChanged,
+} from "@roo-code/core/browser"
+
+export { type ParsedApiReqStartedTextType, getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged }

+ 2 - 0
src/shared/todo.ts

@@ -1,4 +1,5 @@
 import { ClineMessage } from "@roo-code/types"
+
 export function getLatestTodo(clineMessages: ClineMessage[]) {
 	const todos = clineMessages
 		.filter(
@@ -15,6 +16,7 @@ export function getLatestTodo(clineMessages: ClineMessage[]) {
 		.filter((item) => item && item.tool === "updateTodoList" && Array.isArray(item.todos))
 		.map((item) => item.todos)
 		.pop()
+
 	if (todos) {
 		return todos
 	} else {

+ 7 - 5
webview-ui/src/components/chat/BrowserActionRow.tsx

@@ -1,8 +1,5 @@
 import { memo, useMemo, useEffect, useRef } from "react"
-import { ClineMessage } from "@roo-code/types"
-import { ClineSayBrowserAction } from "@roo/ExtensionMessage"
-import { vscode } from "@src/utils/vscode"
-import { getViewportCoordinate as getViewportCoordinateShared, prettyKey } from "@roo/browserUtils"
+import { useTranslation } from "react-i18next"
 import {
 	MousePointer as MousePointerIcon,
 	Keyboard,
@@ -14,8 +11,13 @@ import {
 	Maximize2,
 	Camera,
 } from "lucide-react"
+
+import type { ClineMessage, ClineSayBrowserAction } from "@roo-code/types"
+
+import { getViewportCoordinate as getViewportCoordinateShared, prettyKey } from "@roo/browserUtils"
+
+import { vscode } from "@src/utils/vscode"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { useTranslation } from "react-i18next"
 
 interface BrowserActionRowProps {
 	message: ClineMessage

+ 1 - 2
webview-ui/src/components/chat/BrowserSessionRow.tsx

@@ -2,9 +2,8 @@ import React, { memo, useEffect, useMemo, useRef, useState } from "react"
 import deepEqual from "fast-deep-equal"
 import { useTranslation } from "react-i18next"
 import type { TFunction } from "i18next"
-import type { ClineMessage } from "@roo-code/types"
 
-import { BrowserAction, BrowserActionResult, ClineSayBrowserAction } from "@roo/ExtensionMessage"
+import type { ClineMessage, BrowserAction, BrowserActionResult, ClineSayBrowserAction } from "@roo-code/types"
 
 import { vscode } from "@src/utils/vscode"
 import { useExtensionState } from "@src/context/ExtensionStateContext"

+ 10 - 3
webview-ui/src/components/chat/ChatRow.tsx

@@ -4,12 +4,19 @@ import { useTranslation, Trans } from "react-i18next"
 import deepEqual from "fast-deep-equal"
 import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
 
-import type { ClineMessage, FollowUpData, SuggestionItem } from "@roo-code/types"
+import type {
+	ClineMessage,
+	FollowUpData,
+	SuggestionItem,
+	ClineApiReqInfo,
+	ClineAskUseMcpServer,
+	ClineSayTool,
+} from "@roo-code/types"
+
 import { Mode } from "@roo/modes"
 
-import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
 import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
-import { safeJsonParse } from "@roo/safeJsonParse"
+import { safeJsonParse } from "@roo/core"
 
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { findMatchingResourceOrTemplate } from "@src/utils/mcp"

+ 1 - 2
webview-ui/src/components/chat/ChatView.tsx

@@ -11,9 +11,8 @@ import { Trans } from "react-i18next"
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
 import { appendImages } from "@src/utils/imageUtils"
 
-import type { ClineAsk, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
+import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
 
-import { ClineSayTool } from "@roo/ExtensionMessage"
 import { findLast } from "@roo/array"
 import { SuggestionItem } from "@roo-code/types"
 import { combineApiRequests } from "@roo/combineApiRequests"

+ 1 - 4
webview-ui/src/components/chat/CodeIndexPopover.tsx

@@ -12,10 +12,7 @@ import {
 import * as ProgressPrimitive from "@radix-ui/react-progress"
 import { AlertTriangle } from "lucide-react"
 
-import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
-
-import type { EmbedderProvider } from "@roo/embeddingModels"
-import type { IndexingStatus } from "@roo/ExtensionMessage"
+import { type IndexingStatus, type EmbedderProvider, CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
 
 import { vscode } from "@src/utils/vscode"
 import { useExtensionState } from "@src/context/ExtensionStateContext"

+ 1 - 1
webview-ui/src/components/chat/CommandExecution.tsx

@@ -5,7 +5,7 @@ import { ChevronDown, OctagonX } from "lucide-react"
 
 import { type ExtensionMessage, type CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/types"
 
-import { safeJsonParse } from "@roo/safeJsonParse"
+import { safeJsonParse } from "@roo/core"
 import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
 import { parseCommand } from "@roo/parse-command"
 

+ 2 - 2
webview-ui/src/components/chat/IndexingStatusBadge.tsx

@@ -1,12 +1,12 @@
 import React, { useState, useEffect, useMemo } from "react"
 import { Database } from "lucide-react"
 
+import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo-code/types"
+
 import { cn } from "@src/lib/utils"
 import { vscode } from "@src/utils/vscode"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 
-import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage"
-
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui"
 

+ 8 - 3
webview-ui/src/components/chat/McpExecution.tsx

@@ -3,13 +3,18 @@ import { Server, ChevronDown } from "lucide-react"
 import { useEvent } from "react-use"
 import { useTranslation } from "react-i18next"
 
-import { type ExtensionMessage, type McpExecutionStatus, mcpExecutionStatusSchema } from "@roo-code/types"
+import {
+	type ExtensionMessage,
+	type ClineAskUseMcpServer,
+	type McpExecutionStatus,
+	mcpExecutionStatusSchema,
+} from "@roo-code/types"
+
+import { safeJsonParse } from "@roo/core"
 
 import { cn } from "@src/lib/utils"
 import { Button } from "@src/components/ui"
 
-import { ClineAskUseMcpServer } from "../../../../src/shared/ExtensionMessage"
-import { safeJsonParse } from "../../../../src/shared/safeJsonParse"
 import CodeBlock from "../common/CodeBlock"
 import McpToolRow from "../mcp/McpToolRow"