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

This commit is contained in:
Chris Estreich 2026-01-09 13:05:15 -08:00 committed by GitHub
parent 3171ffc809
commit a4eb15b5a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1343 additions and 632 deletions

View file

@ -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",

View file

@ -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
packages/core/src/cli.ts Normal file
View file

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

View file

@ -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")

View file

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

View file

@ -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)
})
})

View file

@ -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([])
})
})
})

View file

@ -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)
})
})

View file

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

View file

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

View file

@ -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
})
}

View file

@ -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"

View file

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

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -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()

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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", () => {

View file

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

View file

@ -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"

View file

@ -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

View file

@ -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"
/**

View file

@ -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"

View file

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

View file

@ -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
src/shared/core.ts Normal file
View file

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

View file

@ -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 = {

View file

@ -1,156 +1,8 @@
import type { TokenUsage, ToolUsage, ToolName, ClineMessage } from "@roo-code/types"
import {
type ParsedApiReqStartedTextType,
consolidateTokenUsage as getApiMetrics,
hasTokenUsageChanged,
hasToolUsageChanged,
} from "@roo-code/core/browser"
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
})
}
export { type ParsedApiReqStartedTextType, getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged }

View file

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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"