diff --git a/packages/core/package.json b/packages/core/package.json index 5151d88d951..95c6d793b35 100644 --- a/packages/core/package.json +++ b/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", diff --git a/packages/core/src/browser.ts b/packages/core/src/browser.ts new file mode 100644 index 00000000000..d332c9d403e --- /dev/null +++ b/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" diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts new file mode 100644 index 00000000000..7826c0da380 --- /dev/null +++ b/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" diff --git a/packages/core/src/debug-log/index.ts b/packages/core/src/debug-log/index.ts new file mode 100644 index 00000000000..797ec60ba95 --- /dev/null +++ b/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") diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fd7c93f68a1..937f71063bf 100644 --- a/packages/core/src/index.ts +++ b/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" diff --git a/packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts b/packages/core/src/message-utils/__tests__/consolidateApiRequests.spec.ts new file mode 100644 index 00000000000..1ee5cf3b7d0 --- /dev/null +++ b/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 = {}): 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 = {}): 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) + }) +}) diff --git a/packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts b/packages/core/src/message-utils/__tests__/consolidateCommands.spec.ts new file mode 100644 index 00000000000..ae73dc89793 --- /dev/null +++ b/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([]) + }) + }) +}) diff --git a/packages/core/src/message-utils/__tests__/consolidateTokenUsage.spec.ts b/packages/core/src/message-utils/__tests__/consolidateTokenUsage.spec.ts new file mode 100644 index 00000000000..e95ef61b076 --- /dev/null +++ b/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) + }) +}) diff --git a/packages/core/src/message-utils/consolidateApiRequests.ts b/packages/core/src/message-utils/consolidateApiRequests.ts new file mode 100644 index 00000000000..ee538e015ec --- /dev/null +++ b/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 +} diff --git a/packages/core/src/message-utils/consolidateCommands.ts b/packages/core/src/message-utils/consolidateCommands.ts new file mode 100644 index 00000000000..32527d486a3 --- /dev/null +++ b/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() + const processedIndices = new Set() + + // 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(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 +} diff --git a/packages/core/src/message-utils/consolidateTokenUsage.ts b/packages/core/src/message-utils/consolidateTokenUsage.ts new file mode 100644 index 00000000000..ca643afd178 --- /dev/null +++ b/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 + }) +} diff --git a/packages/core/src/message-utils/index.ts b/packages/core/src/message-utils/index.ts new file mode 100644 index 00000000000..b73600ea77f --- /dev/null +++ b/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" diff --git a/src/shared/safeJsonParse.ts b/packages/core/src/message-utils/safeJsonParse.ts similarity index 81% rename from src/shared/safeJsonParse.ts rename to packages/core/src/message-utils/safeJsonParse.ts index 7ca4eee06d0..c60f8b3b84f 100644 --- a/src/shared/safeJsonParse.ts +++ b/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(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 } diff --git a/packages/types/src/embedding.ts b/packages/types/src/embedding.ts new file mode 100644 index 00000000000..1c5a92e1acb --- /dev/null +++ b/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 + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c3c2b937743..2ed3b00ac99 100644 --- a/packages/types/src/index.ts +++ b/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" diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 81018ee44a1..b80f1b09fd6 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/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" diff --git a/packages/vscode-shim/src/api/create-vscode-api-mock.ts b/packages/vscode-shim/src/api/create-vscode-api-mock.ts index 1eb22d3675d..fd4a94a8a61 100644 --- a/packages/vscode-shim/src/api/create-vscode-api-mock.ts +++ b/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() diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index cbfae08f41e..977ce5cbdee 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/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" diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 1da8273a142..5e70b504fbf 100644 --- a/src/api/providers/gemini.ts +++ b/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" diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts index 3d340c98f17..f2951405010 100644 --- a/src/core/auto-approval/index.ts +++ b/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" diff --git a/src/core/auto-approval/tools.ts b/src/core/auto-approval/tools.ts index 4e27a217a95..a43f0cd994e 100644 --- a/src/core/auto-approval/tools.ts +++ b/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) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 64a8ad1cfe5..26a137b939c 100644 --- a/src/core/checkpoints/index.ts +++ b/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" diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2cbcf0c29d4..d3239983af9 100644 --- a/src/core/task/Task.ts +++ b/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" diff --git a/src/core/tools/ApplyDiffTool.ts b/src/core/tools/ApplyDiffTool.ts index 7d026b2f3c8..c8024c7500b 100644 --- a/src/core/tools/ApplyDiffTool.ts +++ b/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 diff --git a/src/core/tools/ApplyPatchTool.ts b/src/core/tools/ApplyPatchTool.ts index 000bc14729e..bf4cdaa1b8c 100644 --- a/src/core/tools/ApplyPatchTool.ts +++ b/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" diff --git a/src/core/tools/AskFollowupQuestionTool.ts b/src/core/tools/AskFollowupQuestionTool.ts index 16caf39992a..b75ca3b618e 100644 --- a/src/core/tools/AskFollowupQuestionTool.ts +++ b/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 diff --git a/src/core/tools/AttemptCompletionTool.ts b/src/core/tools/AttemptCompletionTool.ts index 77677b731b5..039036f829c 100644 --- a/src/core/tools/AttemptCompletionTool.ts +++ b/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 diff --git a/src/core/tools/BaseTool.ts b/src/core/tools/BaseTool.ts index 764cde59c59..e18c3593e43 100644 --- a/src/core/tools/BaseTool.ts +++ b/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 diff --git a/src/core/tools/BrowserActionTool.ts b/src/core/tools/BrowserActionTool.ts index d1e8c678763..39a2bab3d1a 100644 --- a/src/core/tools/BrowserActionTool.ts +++ b/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( diff --git a/src/core/tools/CodebaseSearchTool.ts b/src/core/tools/CodebaseSearchTool.ts index 23cd239f384..96b5cb5d088 100644 --- a/src/core/tools/CodebaseSearchTool.ts +++ b/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 diff --git a/src/core/tools/EditFileTool.ts b/src/core/tools/EditFileTool.ts index f710f6f5636..f8fe3dc0d0f 100644 --- a/src/core/tools/EditFileTool.ts +++ b/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 diff --git a/src/core/tools/FetchInstructionsTool.ts b/src/core/tools/FetchInstructionsTool.ts index d12b434bb0c..7749de2cb8d 100644 --- a/src/core/tools/FetchInstructionsTool.ts +++ b/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 } diff --git a/src/core/tools/ListFilesTool.ts b/src/core/tools/ListFilesTool.ts index 37e3676a03b..b4128d2a851 100644 --- a/src/core/tools/ListFilesTool.ts +++ b/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 diff --git a/src/core/tools/MultiApplyDiffTool.ts b/src/core/tools/MultiApplyDiffTool.ts index 94cdb3fd492..af5fefa251c 100644 --- a/src/core/tools/MultiApplyDiffTool.ts +++ b/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 { diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index 483d4f00252..2bba6bc6cd9 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/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 diff --git a/src/core/tools/SearchAndReplaceTool.ts b/src/core/tools/SearchAndReplaceTool.ts index 9c1e6e553fe..724f2d08229 100644 --- a/src/core/tools/SearchAndReplaceTool.ts +++ b/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 diff --git a/src/core/tools/SearchFilesTool.ts b/src/core/tools/SearchFilesTool.ts index ee8a946bf65..ad1ea22b8fc 100644 --- a/src/core/tools/SearchFilesTool.ts +++ b/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 diff --git a/src/core/tools/SearchReplaceTool.ts b/src/core/tools/SearchReplaceTool.ts index 26774c96c21..e95427bde73 100644 --- a/src/core/tools/SearchReplaceTool.ts +++ b/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 diff --git a/src/core/tools/UseMcpToolTool.ts b/src/core/tools/UseMcpToolTool.ts index b44b96054ee..e7ed744c78c 100644 --- a/src/core/tools/UseMcpToolTool.ts +++ b/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 diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index 29e808b490e..11247ec03d6 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/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 diff --git a/src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts b/src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts index e4f9870c752..5f3dd271b2d 100644 --- a/src/core/tools/__tests__/BrowserActionTool.screenshot.spec.ts +++ b/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", () => { diff --git a/src/core/tools/accessMcpResourceTool.ts b/src/core/tools/accessMcpResourceTool.ts index ff6705ef6cf..65b0e410782 100644 --- a/src/core/tools/accessMcpResourceTool.ts +++ b/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 { diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 81e4982124a..3645c1e153c 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/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" diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 7f44109bf54..7ab7e88cad5 100644 --- a/src/services/browser/BrowserSession.ts +++ b/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 diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index c98c65d4c19..d23eff4810b 100644 --- a/src/services/code-index/service-factory.ts +++ b/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" /** diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts deleted file mode 100644 index 4a29784a402..00000000000 --- a/src/shared/ExtensionMessage.ts +++ /dev/null @@ -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" diff --git a/src/shared/combineApiRequests.ts b/src/shared/combineApiRequests.ts index 20ba6bb6aa1..4807e9a0099 100644 --- a/src/shared/combineApiRequests.ts +++ b/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 } diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts index 56b97a368e5..18d55dfd776 100644 --- a/src/shared/combineCommandSequences.ts +++ b/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() - const processedIndices = new Set() - - // 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(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 } diff --git a/src/shared/core.ts b/src/shared/core.ts new file mode 100644 index 00000000000..fe839be74f8 --- /dev/null +++ b/src/shared/core.ts @@ -0,0 +1 @@ +export * from "@roo-code/core/browser" diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index 781a4e46560..a4c5217a9d2 100644 --- a/src/shared/embeddingModels.ts +++ b/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 = { diff --git a/src/shared/getApiMetrics.ts b/src/shared/getApiMetrics.ts index 072c9da6d3f..50d87e8f52b 100644 --- a/src/shared/getApiMetrics.ts +++ b/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 } diff --git a/src/shared/todo.ts b/src/shared/todo.ts index 16e7d085e2f..d20539049b0 100644 --- a/src/shared/todo.ts +++ b/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 { diff --git a/webview-ui/src/components/chat/BrowserActionRow.tsx b/webview-ui/src/components/chat/BrowserActionRow.tsx index ae4fe3915b6..abc09832804 100644 --- a/webview-ui/src/components/chat/BrowserActionRow.tsx +++ b/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 diff --git a/webview-ui/src/components/chat/BrowserSessionRow.tsx b/webview-ui/src/components/chat/BrowserSessionRow.tsx index cbaf24e8edc..cf67abdc586 100644 --- a/webview-ui/src/components/chat/BrowserSessionRow.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index ef2231b26d1..e36ef2811bb 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index e92d998d810..709218c935f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 368f0395eaf..4fcf6406e3b 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/CommandExecution.tsx b/webview-ui/src/components/chat/CommandExecution.tsx index 716272eaf15..e5763213cc4 100644 --- a/webview-ui/src/components/chat/CommandExecution.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 5d4818a09af..82f654a82fa 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/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" diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx index 6151e581ba9..9e48552fdc8 100644 --- a/webview-ui/src/components/chat/McpExecution.tsx +++ b/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"