mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Merge branch 'main' into improve_diff_prompt
This commit is contained in:
@@ -13,7 +13,13 @@ import { ApiHandler, SingleCompletionHandler, buildApiHandler } from "../api"
|
||||
import { ApiStream } from "../api/transform/stream"
|
||||
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
|
||||
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
|
||||
import { extractTextFromFile, addLineNumbers, stripLineNumbers, everyLineHasLineNumbers, truncateOutput } from "../integrations/misc/extract-text"
|
||||
import {
|
||||
extractTextFromFile,
|
||||
addLineNumbers,
|
||||
stripLineNumbers,
|
||||
everyLineHasLineNumbers,
|
||||
truncateOutput,
|
||||
} from "../integrations/misc/extract-text"
|
||||
import { TerminalManager } from "../integrations/terminal/TerminalManager"
|
||||
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
|
||||
import { listFiles } from "../services/glob/list-files"
|
||||
@@ -45,7 +51,8 @@ import { arePathsEqual, getReadablePath } from "../utils/path"
|
||||
import { parseMentions } from "./mentions"
|
||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
||||
import { formatResponse } from "./prompts/responses"
|
||||
import { addCustomInstructions, codeMode, SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { modes, defaultModeSlug } from "../shared/modes"
|
||||
import { truncateHalfConversation } from "./sliding-window"
|
||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||
import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
||||
@@ -111,7 +118,7 @@ export class Cline {
|
||||
experimentalDiffStrategy: boolean = false,
|
||||
) {
|
||||
if (!task && !images && !historyItem) {
|
||||
throw new Error('Either historyItem or task/images must be provided');
|
||||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
|
||||
this.taskId = crypto.randomUUID()
|
||||
@@ -143,7 +150,8 @@ export class Cline {
|
||||
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
|
||||
// If not provided, get from current state
|
||||
if (experimentalDiffStrategy === undefined) {
|
||||
const { experimentalDiffStrategy: stateExperimentalDiffStrategy } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const { experimentalDiffStrategy: stateExperimentalDiffStrategy } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
experimentalDiffStrategy = stateExperimentalDiffStrategy ?? false
|
||||
}
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
|
||||
@@ -470,7 +478,7 @@ export class Cline {
|
||||
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
|
||||
|
||||
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] =
|
||||
await this.getSavedApiConversationHistory()
|
||||
await this.getSavedApiConversationHistory()
|
||||
|
||||
// Now present the cline messages to the user and ask if they want to resume
|
||||
|
||||
@@ -581,8 +589,8 @@ export class Cline {
|
||||
: [{ type: "text", text: lastMessage.content }]
|
||||
if (previousAssistantMessage && previousAssistantMessage.role === "assistant") {
|
||||
const assistantContent = Array.isArray(previousAssistantMessage.content)
|
||||
? previousAssistantMessage.content
|
||||
: [{ type: "text", text: previousAssistantMessage.content }]
|
||||
? previousAssistantMessage.content
|
||||
: [{ type: "text", text: previousAssistantMessage.content }]
|
||||
|
||||
const toolUseBlocks = assistantContent.filter(
|
||||
(block) => block.type === "tool_use",
|
||||
@@ -755,8 +763,8 @@ export class Cline {
|
||||
// grouping command_output messages despite any gaps anyways)
|
||||
await delay(50)
|
||||
|
||||
const { terminalOutputLineLimit } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const output = truncateOutput(lines.join('\n'), terminalOutputLineLimit)
|
||||
const { terminalOutputLineLimit } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const output = truncateOutput(lines.join("\n"), terminalOutputLineLimit)
|
||||
const result = output.trim()
|
||||
|
||||
if (userFeedback) {
|
||||
@@ -787,7 +795,8 @@ export class Cline {
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
let mcpHub: McpHub | undefined
|
||||
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
if (mcpEnabled ?? true) {
|
||||
mcpHub = this.providerRef.deref()?.mcpHub
|
||||
@@ -800,24 +809,27 @@ export class Cline {
|
||||
})
|
||||
}
|
||||
|
||||
const { browserViewportSize, preferredLanguage, mode, customPrompts } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const systemPrompt = await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
this.api.getModel().info.supportsComputerUse ?? false,
|
||||
mcpHub,
|
||||
this.diffStrategy,
|
||||
browserViewportSize,
|
||||
mode,
|
||||
customPrompts
|
||||
) + await addCustomInstructions(
|
||||
{
|
||||
customInstructions: this.customInstructions,
|
||||
const { browserViewportSize, preferredLanguage, mode, customPrompts } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
const systemPrompt =
|
||||
(await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
this.api.getModel().info.supportsComputerUse ?? false,
|
||||
mcpHub,
|
||||
this.diffStrategy,
|
||||
browserViewportSize,
|
||||
mode,
|
||||
customPrompts,
|
||||
preferredLanguage
|
||||
},
|
||||
cwd,
|
||||
mode
|
||||
)
|
||||
)) +
|
||||
(await addCustomInstructions(
|
||||
{
|
||||
customInstructions: this.customInstructions,
|
||||
customPrompts,
|
||||
preferredLanguage,
|
||||
},
|
||||
cwd,
|
||||
mode,
|
||||
))
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
if (previousApiReqIndex >= 0) {
|
||||
@@ -844,18 +856,18 @@ export class Cline {
|
||||
if (Array.isArray(content)) {
|
||||
if (!this.api.getModel().info.supportsImages) {
|
||||
// Convert image blocks to text descriptions
|
||||
content = content.map(block => {
|
||||
if (block.type === 'image') {
|
||||
content = content.map((block) => {
|
||||
if (block.type === "image") {
|
||||
// Convert image blocks to text descriptions
|
||||
// Note: We can't access the actual image content/url due to API limitations,
|
||||
// but we can indicate that an image was present in the conversation
|
||||
return {
|
||||
type: 'text',
|
||||
text: '[Referenced image in conversation]'
|
||||
};
|
||||
type: "text",
|
||||
text: "[Referenced image in conversation]",
|
||||
}
|
||||
}
|
||||
return block;
|
||||
});
|
||||
return block
|
||||
})
|
||||
}
|
||||
}
|
||||
return { role, content }
|
||||
@@ -875,7 +887,12 @@ export class Cline {
|
||||
// Automatically retry with delay
|
||||
// Show countdown timer in error color
|
||||
for (let i = requestDelay; i > 0; i--) {
|
||||
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying in ${i} seconds...`, undefined, true)
|
||||
await this.say(
|
||||
"api_req_retry_delayed",
|
||||
`${errorMsg}\n\nRetrying in ${i} seconds...`,
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
await delay(1000)
|
||||
}
|
||||
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false)
|
||||
@@ -1124,9 +1141,9 @@ export class Cline {
|
||||
}
|
||||
|
||||
// Validate tool use based on current mode
|
||||
const { mode } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
try {
|
||||
validateToolUse(block.name, mode ?? codeMode)
|
||||
validateToolUse(block.name, mode ?? defaultModeSlug)
|
||||
} catch (error) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(formatResponse.toolError(error.message))
|
||||
@@ -1191,7 +1208,10 @@ export class Cline {
|
||||
await this.diffViewProvider.open(relPath)
|
||||
}
|
||||
// editor is open, stream content in
|
||||
await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, false)
|
||||
await this.diffViewProvider.update(
|
||||
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
|
||||
false,
|
||||
)
|
||||
break
|
||||
} else {
|
||||
if (!relPath) {
|
||||
@@ -1208,7 +1228,9 @@ export class Cline {
|
||||
}
|
||||
if (!predictedLineCount) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "line_count"))
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("write_to_file", "line_count"),
|
||||
)
|
||||
await this.diffViewProvider.reset()
|
||||
break
|
||||
}
|
||||
@@ -1223,17 +1245,28 @@ export class Cline {
|
||||
await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
|
||||
await this.diffViewProvider.open(relPath)
|
||||
}
|
||||
await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, true)
|
||||
await this.diffViewProvider.update(
|
||||
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
|
||||
true,
|
||||
)
|
||||
await delay(300) // wait for diff view to update
|
||||
this.diffViewProvider.scrollToFirstDiff()
|
||||
|
||||
// Check for code omissions before proceeding
|
||||
if (detectCodeOmission(this.diffViewProvider.originalContent || "", newContent, predictedLineCount)) {
|
||||
if (
|
||||
detectCodeOmission(
|
||||
this.diffViewProvider.originalContent || "",
|
||||
newContent,
|
||||
predictedLineCount,
|
||||
)
|
||||
) {
|
||||
if (this.diffStrategy) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
pushToolResult(formatResponse.toolError(
|
||||
`Content appears to be truncated (file has ${newContent.split("\n").length} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`
|
||||
))
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`Content appears to be truncated (file has ${newContent.split("\n").length} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
|
||||
),
|
||||
)
|
||||
break
|
||||
} else {
|
||||
vscode.window
|
||||
@@ -1284,7 +1317,7 @@ export class Cline {
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
|
||||
`Please note:\n` +
|
||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||
@@ -1346,21 +1379,24 @@ export class Cline {
|
||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||
|
||||
// Apply the diff to the original content
|
||||
const diffResult = await this.diffStrategy?.applyDiff(
|
||||
originalContent,
|
||||
diffContent,
|
||||
parseInt(block.params.start_line ?? ''),
|
||||
parseInt(block.params.end_line ?? '')
|
||||
) ?? {
|
||||
const diffResult = (await this.diffStrategy?.applyDiff(
|
||||
originalContent,
|
||||
diffContent,
|
||||
parseInt(block.params.start_line ?? ""),
|
||||
parseInt(block.params.end_line ?? ""),
|
||||
)) ?? {
|
||||
success: false,
|
||||
error: "No diff strategy available"
|
||||
error: "No diff strategy available",
|
||||
}
|
||||
if (!diffResult.success) {
|
||||
this.consecutiveMistakeCount++
|
||||
const currentCount = (this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
||||
const currentCount =
|
||||
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
||||
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
|
||||
const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : ''
|
||||
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${diffResult.error}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ''}\n</error_details>`
|
||||
const errorDetails = diffResult.details
|
||||
? JSON.stringify(diffResult.details, null, 2)
|
||||
: ""
|
||||
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${diffResult.error}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
if (currentCount >= 2) {
|
||||
await this.say("error", formattedError)
|
||||
}
|
||||
@@ -1372,9 +1408,9 @@ export class Cline {
|
||||
this.consecutiveMistakeCountForApplyDiff.delete(relPath)
|
||||
// Show diff view before asking for approval
|
||||
this.diffViewProvider.editType = "modify"
|
||||
await this.diffViewProvider.open(relPath);
|
||||
await this.diffViewProvider.update(diffResult.content, true);
|
||||
await this.diffViewProvider.scrollToFirstDiff();
|
||||
await this.diffViewProvider.open(relPath)
|
||||
await this.diffViewProvider.update(diffResult.content, true)
|
||||
await this.diffViewProvider.scrollToFirstDiff()
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
@@ -1402,7 +1438,7 @@ export class Cline {
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || '')}\n</final_file_content>\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
|
||||
`Please note:\n` +
|
||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||
@@ -1410,7 +1446,9 @@ export class Cline {
|
||||
`${newProblemsMessage}`,
|
||||
)
|
||||
} else {
|
||||
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`)
|
||||
pushToolResult(
|
||||
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
|
||||
)
|
||||
}
|
||||
await this.diffViewProvider.reset()
|
||||
break
|
||||
@@ -1614,7 +1652,7 @@ export class Cline {
|
||||
await this.ask(
|
||||
"browser_action_launch",
|
||||
removeClosingTag("url", url),
|
||||
block.partial
|
||||
block.partial,
|
||||
).catch(() => {})
|
||||
} else {
|
||||
await this.say(
|
||||
@@ -1743,7 +1781,7 @@ export class Cline {
|
||||
try {
|
||||
if (block.partial) {
|
||||
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
|
||||
() => {}
|
||||
() => {},
|
||||
)
|
||||
break
|
||||
} else {
|
||||
@@ -2408,7 +2446,7 @@ export class Cline {
|
||||
Promise.all(
|
||||
userContent.map(async (block) => {
|
||||
const shouldProcessMentions = (text: string) =>
|
||||
text.includes("<task>") || text.includes("<feedback>");
|
||||
text.includes("<task>") || text.includes("<feedback>")
|
||||
|
||||
if (block.type === "text") {
|
||||
if (shouldProcessMentions(block.text)) {
|
||||
@@ -2417,7 +2455,7 @@ export class Cline {
|
||||
text: await parseMentions(block.text, cwd, this.urlContentFetcher),
|
||||
}
|
||||
}
|
||||
return block;
|
||||
return block
|
||||
} else if (block.type === "tool_result") {
|
||||
if (typeof block.content === "string") {
|
||||
if (shouldProcessMentions(block.content)) {
|
||||
@@ -2426,7 +2464,7 @@ export class Cline {
|
||||
content: await parseMentions(block.content, cwd, this.urlContentFetcher),
|
||||
}
|
||||
}
|
||||
return block;
|
||||
return block
|
||||
} else if (Array.isArray(block.content)) {
|
||||
const parsedContent = await Promise.all(
|
||||
block.content.map(async (contentBlock) => {
|
||||
@@ -2444,7 +2482,7 @@ export class Cline {
|
||||
content: parsedContent,
|
||||
}
|
||||
}
|
||||
return block;
|
||||
return block
|
||||
}
|
||||
return block
|
||||
}),
|
||||
@@ -2570,27 +2608,30 @@ export class Cline {
|
||||
// Add current time information with timezone
|
||||
const now = new Date()
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: true
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
})
|
||||
const timeZone = formatter.resolvedOptions().timeZone
|
||||
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
|
||||
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? '+' : ''}${timeZoneOffset}:00`
|
||||
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : ""}${timeZoneOffset}:00`
|
||||
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
|
||||
|
||||
// Add current mode and any mode-specific warnings
|
||||
const { mode } = await this.providerRef.deref()?.getState() ?? {}
|
||||
const currentMode = mode ?? codeMode
|
||||
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const currentMode = mode ?? defaultModeSlug
|
||||
details += `\n\n# Current Mode\n${currentMode}`
|
||||
|
||||
|
||||
// Add warning if not in code mode
|
||||
if (!isToolAllowedForMode('write_to_file', currentMode) || !isToolAllowedForMode('execute_command', currentMode)) {
|
||||
details += `\n\nNOTE: You are currently in '${currentMode}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to 'code' mode. Note that only the user can switch modes.`
|
||||
if (
|
||||
!isToolAllowedForMode("write_to_file", currentMode) ||
|
||||
!isToolAllowedForMode("execute_command", currentMode)
|
||||
) {
|
||||
details += `\n\nNOTE: You are currently in '${currentMode}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to '${defaultModeSlug}' mode. Note that only the user can switch modes.`
|
||||
}
|
||||
|
||||
if (includeFileDetails) {
|
||||
@@ -2608,4 +2649,4 @@ export class Cline {
|
||||
|
||||
return `<environment_details>\n${details.trim()}\n</environment_details>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +1,52 @@
|
||||
import { isToolAllowedForMode, validateToolUse } from '../mode-validator'
|
||||
import { codeMode, architectMode, askMode } from '../prompts/system'
|
||||
import { CODE_ALLOWED_TOOLS, READONLY_ALLOWED_TOOLS, ToolName } from '../tool-lists'
|
||||
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig, modes } from "../../shared/modes"
|
||||
import { validateToolUse } from "../mode-validator"
|
||||
|
||||
// For testing purposes, we need to handle the 'unknown_tool' case
|
||||
type TestToolName = ToolName | 'unknown_tool';
|
||||
const asTestTool = (tool: string): TestToolName => tool as TestToolName
|
||||
const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug)
|
||||
|
||||
// Helper function to safely cast string to TestToolName for testing
|
||||
function asTestTool(str: string): TestToolName {
|
||||
return str as TestToolName;
|
||||
}
|
||||
describe("mode-validator", () => {
|
||||
describe("isToolAllowedForMode", () => {
|
||||
describe("code mode", () => {
|
||||
it("allows all code mode tools", () => {
|
||||
const mode = getModeConfig(codeMode)
|
||||
mode.tools.forEach(([tool]) => {
|
||||
expect(isToolAllowedForMode(tool, codeMode)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mode-validator', () => {
|
||||
describe('isToolAllowedForMode', () => {
|
||||
describe('code mode', () => {
|
||||
it('allows all code mode tools', () => {
|
||||
CODE_ALLOWED_TOOLS.forEach(tool => {
|
||||
expect(isToolAllowedForMode(tool, codeMode)).toBe(true)
|
||||
})
|
||||
})
|
||||
it("disallows unknown tools", () => {
|
||||
expect(isToolAllowedForMode(asTestTool("unknown_tool"), codeMode)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('disallows unknown tools', () => {
|
||||
expect(isToolAllowedForMode(asTestTool('unknown_tool'), codeMode)).toBe(false)
|
||||
})
|
||||
})
|
||||
describe("architect mode", () => {
|
||||
it("allows configured tools", () => {
|
||||
const mode = getModeConfig(architectMode)
|
||||
mode.tools.forEach(([tool]) => {
|
||||
expect(isToolAllowedForMode(tool, architectMode)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('architect mode', () => {
|
||||
it('allows only read-only and MCP tools', () => {
|
||||
// Test allowed tools
|
||||
READONLY_ALLOWED_TOOLS.forEach(tool => {
|
||||
expect(isToolAllowedForMode(tool, architectMode)).toBe(true)
|
||||
})
|
||||
describe("ask mode", () => {
|
||||
it("allows configured tools", () => {
|
||||
const mode = getModeConfig(askMode)
|
||||
mode.tools.forEach(([tool]) => {
|
||||
expect(isToolAllowedForMode(tool, askMode)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Test specific disallowed tools that we know are in CODE_ALLOWED_TOOLS but not in READONLY_ALLOWED_TOOLS
|
||||
const disallowedTools = ['execute_command', 'write_to_file', 'apply_diff'] as const;
|
||||
disallowedTools.forEach(tool => {
|
||||
expect(isToolAllowedForMode(tool as ToolName, architectMode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("validateToolUse", () => {
|
||||
it("throws error for disallowed tools in architect mode", () => {
|
||||
expect(() => validateToolUse("unknown_tool", "architect")).toThrow(
|
||||
'Tool "unknown_tool" is not allowed in architect mode.',
|
||||
)
|
||||
})
|
||||
|
||||
describe('ask mode', () => {
|
||||
it('allows only read-only and MCP tools', () => {
|
||||
// Test allowed tools
|
||||
READONLY_ALLOWED_TOOLS.forEach(tool => {
|
||||
expect(isToolAllowedForMode(tool, askMode)).toBe(true)
|
||||
})
|
||||
|
||||
// Test specific disallowed tools that we know are in CODE_ALLOWED_TOOLS but not in READONLY_ALLOWED_TOOLS
|
||||
const disallowedTools = ['execute_command', 'write_to_file', 'apply_diff'] as const;
|
||||
disallowedTools.forEach(tool => {
|
||||
expect(isToolAllowedForMode(tool as ToolName, askMode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateToolUse', () => {
|
||||
it('throws error for disallowed tools in architect mode', () => {
|
||||
expect(() => validateToolUse('write_to_file' as ToolName, architectMode)).toThrow(
|
||||
'Tool "write_to_file" is not allowed in architect mode.'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error for disallowed tools in ask mode', () => {
|
||||
expect(() => validateToolUse('execute_command' as ToolName, askMode)).toThrow(
|
||||
'Tool "execute_command" is not allowed in ask mode.'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error for unknown tools in code mode', () => {
|
||||
expect(() => validateToolUse(asTestTool('unknown_tool'), codeMode)).toThrow(
|
||||
'Tool "unknown_tool" is not allowed in code mode.'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not throw for allowed tools', () => {
|
||||
// Code mode
|
||||
expect(() => validateToolUse('write_to_file' as ToolName, codeMode)).not.toThrow()
|
||||
|
||||
// Architect mode
|
||||
expect(() => validateToolUse('read_file' as ToolName, architectMode)).not.toThrow()
|
||||
|
||||
// Ask mode
|
||||
expect(() => validateToolUse('browser_action' as ToolName, askMode)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
it("does not throw for allowed tools in architect mode", () => {
|
||||
expect(() => validateToolUse("read_file", "architect")).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,221 +1,221 @@
|
||||
import { ExtensionContext } from 'vscode'
|
||||
import { ApiConfiguration } from '../../shared/api'
|
||||
import { Mode } from '../prompts/types'
|
||||
import { ApiConfigMeta } from '../../shared/ExtensionMessage'
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { ApiConfiguration } from "../../shared/api"
|
||||
import { Mode } from "../prompts/types"
|
||||
import { ApiConfigMeta } from "../../shared/ExtensionMessage"
|
||||
|
||||
export interface ApiConfigData {
|
||||
currentApiConfigName: string
|
||||
apiConfigs: {
|
||||
[key: string]: ApiConfiguration
|
||||
}
|
||||
modeApiConfigs?: Partial<Record<Mode, string>>
|
||||
currentApiConfigName: string
|
||||
apiConfigs: {
|
||||
[key: string]: ApiConfiguration
|
||||
}
|
||||
modeApiConfigs?: Partial<Record<Mode, string>>
|
||||
}
|
||||
|
||||
export class ConfigManager {
|
||||
private readonly defaultConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: this.generateId()
|
||||
}
|
||||
}
|
||||
}
|
||||
private readonly defaultConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: this.generateId(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
private readonly SCOPE_PREFIX = "roo_cline_config_"
|
||||
private readonly context: ExtensionContext
|
||||
private readonly SCOPE_PREFIX = "roo_cline_config_"
|
||||
private readonly context: ExtensionContext
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
this.initConfig().catch(console.error)
|
||||
}
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
this.initConfig().catch(console.error)
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
private generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize config if it doesn't exist
|
||||
*/
|
||||
async initConfig(): Promise<void> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
if (!config) {
|
||||
await this.writeConfig(this.defaultConfig)
|
||||
return
|
||||
}
|
||||
/**
|
||||
* Initialize config if it doesn't exist
|
||||
*/
|
||||
async initConfig(): Promise<void> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
if (!config) {
|
||||
await this.writeConfig(this.defaultConfig)
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate: ensure all configs have IDs
|
||||
let needsMigration = false
|
||||
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
|
||||
if (!apiConfig.id) {
|
||||
apiConfig.id = this.generateId()
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
// Migrate: ensure all configs have IDs
|
||||
let needsMigration = false
|
||||
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
|
||||
if (!apiConfig.id) {
|
||||
apiConfig.id = this.generateId()
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await this.writeConfig(config)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize config: ${error}`)
|
||||
}
|
||||
}
|
||||
if (needsMigration) {
|
||||
await this.writeConfig(config)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available configs with metadata
|
||||
*/
|
||||
async ListConfig(): Promise<ApiConfigMeta[]> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
|
||||
name,
|
||||
id: apiConfig.id || '',
|
||||
apiProvider: apiConfig.apiProvider,
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list configs: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List all available configs with metadata
|
||||
*/
|
||||
async ListConfig(): Promise<ApiConfigMeta[]> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
|
||||
name,
|
||||
id: apiConfig.id || "",
|
||||
apiProvider: apiConfig.apiProvider,
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list configs: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a config with the given name
|
||||
*/
|
||||
async SaveConfig(name: string, config: ApiConfiguration): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
const existingConfig = currentConfig.apiConfigs[name]
|
||||
currentConfig.apiConfigs[name] = {
|
||||
...config,
|
||||
id: existingConfig?.id || this.generateId()
|
||||
}
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save config: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Save a config with the given name
|
||||
*/
|
||||
async SaveConfig(name: string, config: ApiConfiguration): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
const existingConfig = currentConfig.apiConfigs[name]
|
||||
currentConfig.apiConfigs[name] = {
|
||||
...config,
|
||||
id: existingConfig?.id || this.generateId(),
|
||||
}
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a config by name
|
||||
*/
|
||||
async LoadConfig(name: string): Promise<ApiConfiguration> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
const apiConfig = config.apiConfigs[name]
|
||||
|
||||
if (!apiConfig) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
config.currentApiConfigName = name;
|
||||
await this.writeConfig(config)
|
||||
|
||||
return apiConfig
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load a config by name
|
||||
*/
|
||||
async LoadConfig(name: string): Promise<ApiConfiguration> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
const apiConfig = config.apiConfigs[name]
|
||||
|
||||
/**
|
||||
* Delete a config by name
|
||||
*/
|
||||
async DeleteConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
if (!apiConfig) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
// Don't allow deleting the default config
|
||||
if (Object.keys(currentConfig.apiConfigs).length === 1) {
|
||||
throw new Error(`Cannot delete the last remaining configuration.`)
|
||||
}
|
||||
config.currentApiConfigName = name
|
||||
await this.writeConfig(config)
|
||||
|
||||
delete currentConfig.apiConfigs[name]
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete config: ${error}`)
|
||||
}
|
||||
}
|
||||
return apiConfig
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current active API configuration
|
||||
*/
|
||||
async SetCurrentConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
/**
|
||||
* Delete a config by name
|
||||
*/
|
||||
async DeleteConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
currentConfig.currentApiConfigName = name
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set current config: ${error}`)
|
||||
}
|
||||
}
|
||||
// Don't allow deleting the default config
|
||||
if (Object.keys(currentConfig.apiConfigs).length === 1) {
|
||||
throw new Error(`Cannot delete the last remaining configuration.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a config exists by name
|
||||
*/
|
||||
async HasConfig(name: string): Promise<boolean> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return name in config.apiConfigs
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to check config existence: ${error}`)
|
||||
}
|
||||
}
|
||||
delete currentConfig.apiConfigs[name]
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the API config for a specific mode
|
||||
*/
|
||||
async SetModeConfig(mode: Mode, configId: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.modeApiConfigs) {
|
||||
currentConfig.modeApiConfigs = {}
|
||||
}
|
||||
currentConfig.modeApiConfigs[mode] = configId
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set mode config: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set the current active API configuration
|
||||
*/
|
||||
async SetCurrentConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API config ID for a specific mode
|
||||
*/
|
||||
async GetModeConfigId(mode: Mode): Promise<string | undefined> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return config.modeApiConfigs?.[mode]
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get mode config: ${error}`)
|
||||
}
|
||||
}
|
||||
currentConfig.currentApiConfigName = name
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set current config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async readConfig(): Promise<ApiConfigData> {
|
||||
try {
|
||||
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||
const content = await this.context.secrets.get(configKey)
|
||||
|
||||
if (!content) {
|
||||
return this.defaultConfig
|
||||
}
|
||||
/**
|
||||
* Check if a config exists by name
|
||||
*/
|
||||
async HasConfig(name: string): Promise<boolean> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return name in config.apiConfigs
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to check config existence: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read config from secrets: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set the API config for a specific mode
|
||||
*/
|
||||
async SetModeConfig(mode: Mode, configId: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.modeApiConfigs) {
|
||||
currentConfig.modeApiConfigs = {}
|
||||
}
|
||||
currentConfig.modeApiConfigs[mode] = configId
|
||||
await this.writeConfig(currentConfig)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set mode config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async writeConfig(config: ApiConfigData): Promise<void> {
|
||||
try {
|
||||
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||
const content = JSON.stringify(config, null, 2)
|
||||
await this.context.secrets.store(configKey, content)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write config to secrets: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the API config ID for a specific mode
|
||||
*/
|
||||
async GetModeConfigId(mode: Mode): Promise<string | undefined> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return config.modeApiConfigs?.[mode]
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get mode config: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async readConfig(): Promise<ApiConfigData> {
|
||||
try {
|
||||
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||
const content = await this.context.secrets.get(configKey)
|
||||
|
||||
if (!content) {
|
||||
return this.defaultConfig
|
||||
}
|
||||
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read config from secrets: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async writeConfig(config: ApiConfigData): Promise<void> {
|
||||
try {
|
||||
const configKey = `${this.SCOPE_PREFIX}api_config`
|
||||
const content = JSON.stringify(config, null, 2)
|
||||
await this.context.secrets.store(configKey, content)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write config to secrets: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,452 +1,470 @@
|
||||
import { ExtensionContext } from 'vscode'
|
||||
import { ConfigManager, ApiConfigData } from '../ConfigManager'
|
||||
import { ApiConfiguration } from '../../../shared/api'
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { ConfigManager, ApiConfigData } from "../ConfigManager"
|
||||
import { ApiConfiguration } from "../../../shared/api"
|
||||
|
||||
// Mock VSCode ExtensionContext
|
||||
const mockSecrets = {
|
||||
get: jest.fn(),
|
||||
store: jest.fn(),
|
||||
delete: jest.fn()
|
||||
get: jest.fn(),
|
||||
store: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
}
|
||||
|
||||
const mockContext = {
|
||||
secrets: mockSecrets
|
||||
secrets: mockSecrets,
|
||||
} as unknown as ExtensionContext
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let configManager: ConfigManager
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
configManager = new ConfigManager(mockContext)
|
||||
})
|
||||
|
||||
describe('initConfig', () => {
|
||||
it('should not write to storage when secrets.get returns null', async () => {
|
||||
// Mock readConfig to return null
|
||||
mockSecrets.get.mockResolvedValueOnce(null)
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
// Should not write to storage because readConfig returns defaultConfig
|
||||
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not initialize config if it exists', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {},
|
||||
id: 'default'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should generate IDs for configs that lack them', async () => {
|
||||
// Mock a config with missing IDs
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {}
|
||||
},
|
||||
test: {
|
||||
apiProvider: 'anthropic'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
// Should have written the config with new IDs
|
||||
expect(mockSecrets.store).toHaveBeenCalled()
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.apiConfigs.default.id).toBeTruthy()
|
||||
expect(storedConfig.apiConfigs.test.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw error if secrets storage fails', async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error('Storage failed'))
|
||||
|
||||
await expect(configManager.initConfig()).rejects.toThrow(
|
||||
'Failed to initialize config: Error: Failed to read config from secrets: Error: Storage failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ListConfig', () => {
|
||||
it('should list all available configs', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: 'default'
|
||||
},
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
id: 'test-id'
|
||||
}
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: 'default',
|
||||
architect: 'default',
|
||||
ask: 'default'
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const configs = await configManager.ListConfig()
|
||||
expect(configs).toEqual([
|
||||
{ name: 'default', id: 'default', apiProvider: undefined },
|
||||
{ name: 'test', id: 'test-id', apiProvider: 'anthropic' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle empty config file', async () => {
|
||||
const emptyConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {},
|
||||
modeApiConfigs: {
|
||||
code: 'default',
|
||||
architect: 'default',
|
||||
ask: 'default'
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
|
||||
|
||||
const configs = await configManager.ListConfig()
|
||||
expect(configs).toEqual([])
|
||||
})
|
||||
|
||||
it('should throw error if reading from secrets fails', async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error('Read failed'))
|
||||
|
||||
await expect(configManager.ListConfig()).rejects.toThrow(
|
||||
'Failed to list configs: Error: Failed to read config from secrets: Error: Read failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SaveConfig', () => {
|
||||
it('should save new config', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {}
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: 'default',
|
||||
architect: 'default',
|
||||
ask: 'default'
|
||||
}
|
||||
}))
|
||||
|
||||
const newConfig: ApiConfiguration = {
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'test-key'
|
||||
}
|
||||
|
||||
await configManager.SaveConfig('test', newConfig)
|
||||
|
||||
// Get the actual stored config to check the generated ID
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
const testConfigId = storedConfig.apiConfigs.test.id
|
||||
|
||||
const expectedConfig = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {},
|
||||
test: {
|
||||
...newConfig,
|
||||
id: testConfigId
|
||||
}
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: 'default',
|
||||
architect: 'default',
|
||||
ask: 'default'
|
||||
}
|
||||
}
|
||||
|
||||
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||
'roo_cline_config_api_config',
|
||||
JSON.stringify(expectedConfig, null, 2)
|
||||
)
|
||||
})
|
||||
|
||||
it('should update existing config', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'old-key',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const updatedConfig: ApiConfiguration = {
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'new-key'
|
||||
}
|
||||
|
||||
await configManager.SaveConfig('test', updatedConfig)
|
||||
|
||||
const expectedConfig = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'new-key',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||
'roo_cline_config_api_config',
|
||||
JSON.stringify(expectedConfig, null, 2)
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error if secrets storage fails', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: { default: {} }
|
||||
}))
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||
|
||||
await expect(configManager.SaveConfig('test', {})).rejects.toThrow(
|
||||
'Failed to save config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DeleteConfig', () => {
|
||||
it('should delete existing config', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: 'default'
|
||||
},
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
await configManager.DeleteConfig('test')
|
||||
|
||||
// Get the stored config to check the ID
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe('default')
|
||||
expect(Object.keys(storedConfig.apiConfigs)).toEqual(['default'])
|
||||
expect(storedConfig.apiConfigs.default.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should throw error when trying to delete non-existent config', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: { default: {} }
|
||||
}))
|
||||
|
||||
await expect(configManager.DeleteConfig('nonexistent')).rejects.toThrow(
|
||||
"Config 'nonexistent' not found"
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when trying to delete last remaining config', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: 'default'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
await expect(configManager.DeleteConfig('default')).rejects.toThrow(
|
||||
'Cannot delete the last remaining configuration.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LoadConfig', () => {
|
||||
it('should load config and update current config name', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'test-key',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const config = await configManager.LoadConfig('test')
|
||||
|
||||
expect(config).toEqual({
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'test-key',
|
||||
id: 'test-id'
|
||||
})
|
||||
|
||||
// Get the stored config to check the structure
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe('test')
|
||||
expect(storedConfig.apiConfigs.test).toEqual({
|
||||
apiProvider: 'anthropic',
|
||||
apiKey: 'test-key',
|
||||
id: 'test-id'
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error when config does not exist', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {},
|
||||
id: 'default'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
await expect(configManager.LoadConfig('nonexistent')).rejects.toThrow(
|
||||
"Config 'nonexistent' not found"
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error if secrets storage fails', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
test: {
|
||||
config: {
|
||||
apiProvider: 'anthropic'
|
||||
},
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}))
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||
|
||||
await expect(configManager.LoadConfig('test')).rejects.toThrow(
|
||||
'Failed to load config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SetCurrentConfig', () => {
|
||||
it('should set current config', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: 'default'
|
||||
},
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
await configManager.SetCurrentConfig('test')
|
||||
|
||||
// Get the stored config to check the structure
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe('test')
|
||||
expect(storedConfig.apiConfigs.default.id).toBe('default')
|
||||
expect(storedConfig.apiConfigs.test).toEqual({
|
||||
apiProvider: 'anthropic',
|
||||
id: 'test-id'
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error when config does not exist', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: { default: {} }
|
||||
}))
|
||||
|
||||
await expect(configManager.SetCurrentConfig('nonexistent')).rejects.toThrow(
|
||||
"Config 'nonexistent' not found"
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error if secrets storage fails', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
test: { apiProvider: 'anthropic' }
|
||||
}
|
||||
}))
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error('Storage failed'))
|
||||
|
||||
await expect(configManager.SetCurrentConfig('test')).rejects.toThrow(
|
||||
'Failed to set current config: Error: Failed to write config to secrets: Error: Storage failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('HasConfig', () => {
|
||||
it('should return true for existing config', async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: 'default'
|
||||
},
|
||||
test: {
|
||||
apiProvider: 'anthropic',
|
||||
id: 'test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const hasConfig = await configManager.HasConfig('test')
|
||||
expect(hasConfig).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-existent config', async () => {
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify({
|
||||
currentApiConfigName: 'default',
|
||||
apiConfigs: { default: {} }
|
||||
}))
|
||||
|
||||
const hasConfig = await configManager.HasConfig('nonexistent')
|
||||
expect(hasConfig).toBe(false)
|
||||
})
|
||||
|
||||
it('should throw error if secrets storage fails', async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error('Storage failed'))
|
||||
|
||||
await expect(configManager.HasConfig('test')).rejects.toThrow(
|
||||
'Failed to check config existence: Error: Failed to read config from secrets: Error: Storage failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("ConfigManager", () => {
|
||||
let configManager: ConfigManager
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
configManager = new ConfigManager(mockContext)
|
||||
})
|
||||
|
||||
describe("initConfig", () => {
|
||||
it("should not write to storage when secrets.get returns null", async () => {
|
||||
// Mock readConfig to return null
|
||||
mockSecrets.get.mockResolvedValueOnce(null)
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
// Should not write to storage because readConfig returns defaultConfig
|
||||
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not initialize config if it exists", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {},
|
||||
id: "default",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
expect(mockSecrets.store).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should generate IDs for configs that lack them", async () => {
|
||||
// Mock a config with missing IDs
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {},
|
||||
},
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await configManager.initConfig()
|
||||
|
||||
// Should have written the config with new IDs
|
||||
expect(mockSecrets.store).toHaveBeenCalled()
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.apiConfigs.default.id).toBeTruthy()
|
||||
expect(storedConfig.apiConfigs.test.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should throw error if secrets storage fails", async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
|
||||
|
||||
await expect(configManager.initConfig()).rejects.toThrow(
|
||||
"Failed to initialize config: Error: Failed to read config from secrets: Error: Storage failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ListConfig", () => {
|
||||
it("should list all available configs", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: "default",
|
||||
},
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "default",
|
||||
architect: "default",
|
||||
ask: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const configs = await configManager.ListConfig()
|
||||
expect(configs).toEqual([
|
||||
{ name: "default", id: "default", apiProvider: undefined },
|
||||
{ name: "test", id: "test-id", apiProvider: "anthropic" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle empty config file", async () => {
|
||||
const emptyConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {},
|
||||
modeApiConfigs: {
|
||||
code: "default",
|
||||
architect: "default",
|
||||
ask: "default",
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
|
||||
|
||||
const configs = await configManager.ListConfig()
|
||||
expect(configs).toEqual([])
|
||||
})
|
||||
|
||||
it("should throw error if reading from secrets fails", async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error("Read failed"))
|
||||
|
||||
await expect(configManager.ListConfig()).rejects.toThrow(
|
||||
"Failed to list configs: Error: Failed to read config from secrets: Error: Read failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SaveConfig", () => {
|
||||
it("should save new config", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {},
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "default",
|
||||
architect: "default",
|
||||
ask: "default",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const newConfig: ApiConfiguration = {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
}
|
||||
|
||||
await configManager.SaveConfig("test", newConfig)
|
||||
|
||||
// Get the actual stored config to check the generated ID
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
const testConfigId = storedConfig.apiConfigs.test.id
|
||||
|
||||
const expectedConfig = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {},
|
||||
test: {
|
||||
...newConfig,
|
||||
id: testConfigId,
|
||||
},
|
||||
},
|
||||
modeApiConfigs: {
|
||||
code: "default",
|
||||
architect: "default",
|
||||
ask: "default",
|
||||
},
|
||||
}
|
||||
|
||||
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||
"roo_cline_config_api_config",
|
||||
JSON.stringify(expectedConfig, null, 2),
|
||||
)
|
||||
})
|
||||
|
||||
it("should update existing config", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "old-key",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const updatedConfig: ApiConfiguration = {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "new-key",
|
||||
}
|
||||
|
||||
await configManager.SaveConfig("test", updatedConfig)
|
||||
|
||||
const expectedConfig = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "new-key",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(mockSecrets.store).toHaveBeenCalledWith(
|
||||
"roo_cline_config_api_config",
|
||||
JSON.stringify(expectedConfig, null, 2),
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if secrets storage fails", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: { default: {} },
|
||||
}),
|
||||
)
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
|
||||
|
||||
await expect(configManager.SaveConfig("test", {})).rejects.toThrow(
|
||||
"Failed to save config: Error: Failed to write config to secrets: Error: Storage failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DeleteConfig", () => {
|
||||
it("should delete existing config", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: "default",
|
||||
},
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
await configManager.DeleteConfig("test")
|
||||
|
||||
// Get the stored config to check the ID
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe("default")
|
||||
expect(Object.keys(storedConfig.apiConfigs)).toEqual(["default"])
|
||||
expect(storedConfig.apiConfigs.default.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should throw error when trying to delete non-existent config", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: { default: {} },
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(configManager.DeleteConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
|
||||
})
|
||||
|
||||
it("should throw error when trying to delete last remaining config", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: "default",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(configManager.DeleteConfig("default")).rejects.toThrow(
|
||||
"Cannot delete the last remaining configuration.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("LoadConfig", () => {
|
||||
it("should load config and update current config name", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const config = await configManager.LoadConfig("test")
|
||||
|
||||
expect(config).toEqual({
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
})
|
||||
|
||||
// Get the stored config to check the structure
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe("test")
|
||||
expect(storedConfig.apiConfigs.test).toEqual({
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
id: "test-id",
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw error when config does not exist", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
config: {},
|
||||
id: "default",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(configManager.LoadConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
|
||||
})
|
||||
|
||||
it("should throw error if secrets storage fails", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
test: {
|
||||
config: {
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
|
||||
|
||||
await expect(configManager.LoadConfig("test")).rejects.toThrow(
|
||||
"Failed to load config: Error: Failed to write config to secrets: Error: Storage failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SetCurrentConfig", () => {
|
||||
it("should set current config", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: "default",
|
||||
},
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
await configManager.SetCurrentConfig("test")
|
||||
|
||||
// Get the stored config to check the structure
|
||||
const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
|
||||
expect(storedConfig.currentApiConfigName).toBe("test")
|
||||
expect(storedConfig.apiConfigs.default.id).toBe("default")
|
||||
expect(storedConfig.apiConfigs.test).toEqual({
|
||||
apiProvider: "anthropic",
|
||||
id: "test-id",
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw error when config does not exist", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: { default: {} },
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(configManager.SetCurrentConfig("nonexistent")).rejects.toThrow(
|
||||
"Config 'nonexistent' not found",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if secrets storage fails", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
test: { apiProvider: "anthropic" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
|
||||
|
||||
await expect(configManager.SetCurrentConfig("test")).rejects.toThrow(
|
||||
"Failed to set current config: Error: Failed to write config to secrets: Error: Storage failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("HasConfig", () => {
|
||||
it("should return true for existing config", async () => {
|
||||
const existingConfig: ApiConfigData = {
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: {
|
||||
default: {
|
||||
id: "default",
|
||||
},
|
||||
test: {
|
||||
apiProvider: "anthropic",
|
||||
id: "test-id",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
|
||||
|
||||
const hasConfig = await configManager.HasConfig("test")
|
||||
expect(hasConfig).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-existent config", async () => {
|
||||
mockSecrets.get.mockResolvedValue(
|
||||
JSON.stringify({
|
||||
currentApiConfigName: "default",
|
||||
apiConfigs: { default: {} },
|
||||
}),
|
||||
)
|
||||
|
||||
const hasConfig = await configManager.HasConfig("nonexistent")
|
||||
expect(hasConfig).toBe(false)
|
||||
})
|
||||
|
||||
it("should throw error if secrets storage fails", async () => {
|
||||
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
|
||||
|
||||
await expect(configManager.HasConfig("test")).rejects.toThrow(
|
||||
"Failed to check config existence: Error: Failed to read config from secrets: Error: Storage failed",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { DiffStrategy } from './types'
|
||||
import { UnifiedDiffStrategy } from './strategies/unified'
|
||||
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||
import { NewUnifiedDiffStrategy } from './strategies/new-unified'
|
||||
import type { DiffStrategy } from "./types"
|
||||
import { UnifiedDiffStrategy } from "./strategies/unified"
|
||||
import { SearchReplaceDiffStrategy } from "./strategies/search-replace"
|
||||
import { NewUnifiedDiffStrategy } from "./strategies/new-unified"
|
||||
/**
|
||||
* Get the appropriate diff strategy for the given model
|
||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||
* @returns The appropriate diff strategy for the model
|
||||
*/
|
||||
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy: boolean = false): DiffStrategy {
|
||||
if (experimentalDiffStrategy) {
|
||||
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
export function getDiffStrategy(
|
||||
model: string,
|
||||
fuzzyMatchThreshold?: number,
|
||||
experimentalDiffStrategy: boolean = false,
|
||||
): DiffStrategy {
|
||||
if (experimentalDiffStrategy) {
|
||||
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
|
||||
export type { DiffStrategy }
|
||||
|
||||
@@ -1,74 +1,73 @@
|
||||
import { NewUnifiedDiffStrategy } from '../new-unified';
|
||||
import { NewUnifiedDiffStrategy } from "../new-unified"
|
||||
|
||||
describe('main', () => {
|
||||
describe("main", () => {
|
||||
let strategy: NewUnifiedDiffStrategy
|
||||
|
||||
let strategy: NewUnifiedDiffStrategy
|
||||
beforeEach(() => {
|
||||
strategy = new NewUnifiedDiffStrategy(0.97)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new NewUnifiedDiffStrategy(0.97)
|
||||
})
|
||||
describe("constructor", () => {
|
||||
it("should use default confidence threshold when not provided", () => {
|
||||
const defaultStrategy = new NewUnifiedDiffStrategy()
|
||||
expect(defaultStrategy["confidenceThreshold"]).toBe(1)
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should use default confidence threshold when not provided', () => {
|
||||
const defaultStrategy = new NewUnifiedDiffStrategy()
|
||||
expect(defaultStrategy['confidenceThreshold']).toBe(1)
|
||||
})
|
||||
it("should use provided confidence threshold", () => {
|
||||
const customStrategy = new NewUnifiedDiffStrategy(0.85)
|
||||
expect(customStrategy["confidenceThreshold"]).toBe(0.85)
|
||||
})
|
||||
|
||||
it('should use provided confidence threshold', () => {
|
||||
const customStrategy = new NewUnifiedDiffStrategy(0.85)
|
||||
expect(customStrategy['confidenceThreshold']).toBe(0.85)
|
||||
})
|
||||
it("should enforce minimum confidence threshold", () => {
|
||||
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
|
||||
expect(lowStrategy["confidenceThreshold"]).toBe(0.8)
|
||||
})
|
||||
})
|
||||
|
||||
it('should enforce minimum confidence threshold', () => {
|
||||
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
|
||||
expect(lowStrategy['confidenceThreshold']).toBe(0.8)
|
||||
})
|
||||
})
|
||||
describe("getToolDescription", () => {
|
||||
it("should return tool description with correct cwd", () => {
|
||||
const cwd = "/test/path"
|
||||
const description = strategy.getToolDescription({ cwd })
|
||||
|
||||
describe('getToolDescription', () => {
|
||||
it('should return tool description with correct cwd', () => {
|
||||
const cwd = '/test/path'
|
||||
const description = strategy.getToolDescription(cwd)
|
||||
|
||||
expect(description).toContain('apply_diff')
|
||||
expect(description).toContain(cwd)
|
||||
expect(description).toContain('Parameters:')
|
||||
expect(description).toContain('Format Requirements:')
|
||||
})
|
||||
})
|
||||
expect(description).toContain("apply_diff")
|
||||
expect(description).toContain(cwd)
|
||||
expect(description).toContain("Parameters:")
|
||||
expect(description).toContain("Format Requirements:")
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply simple diff correctly', async () => {
|
||||
const original = `line1
|
||||
it("should apply simple diff correctly", async () => {
|
||||
const original = `line1
|
||||
line2
|
||||
line3`;
|
||||
line3`
|
||||
|
||||
const diff = `--- a/file.txt
|
||||
const diff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
+new line
|
||||
line2
|
||||
-line3
|
||||
+modified line3`;
|
||||
+modified line3`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if(result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
new line
|
||||
line2
|
||||
modified line3`);
|
||||
}
|
||||
});
|
||||
modified line3`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle multiple hunks', async () => {
|
||||
const original = `line1
|
||||
it("should handle multiple hunks", async () => {
|
||||
const original = `line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
line5`;
|
||||
line5`
|
||||
|
||||
const diff = `--- a/file.txt
|
||||
const diff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
@@ -80,23 +79,23 @@ line5`;
|
||||
line4
|
||||
-line5
|
||||
+modified line5
|
||||
+new line at end`;
|
||||
+new line at end`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
new line
|
||||
line2
|
||||
modified line3
|
||||
line4
|
||||
modified line5
|
||||
new line at end`);
|
||||
}
|
||||
});
|
||||
new line at end`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle complex large', async () => {
|
||||
const original = `line1
|
||||
it("should handle complex large", async () => {
|
||||
const original = `line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
@@ -105,9 +104,9 @@ line6
|
||||
line7
|
||||
line8
|
||||
line9
|
||||
line10`;
|
||||
line10`
|
||||
|
||||
const diff = `--- a/file.txt
|
||||
const diff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
@@ -130,12 +129,12 @@ line10`;
|
||||
line9
|
||||
-line10
|
||||
+final line
|
||||
+very last line`;
|
||||
+very last line`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
header line
|
||||
another header
|
||||
line2
|
||||
@@ -150,12 +149,12 @@ changed line8
|
||||
bonus line
|
||||
line9
|
||||
final line
|
||||
very last line`);
|
||||
}
|
||||
});
|
||||
very last line`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle indentation changes', async () => {
|
||||
const original = `first line
|
||||
it("should handle indentation changes", async () => {
|
||||
const original = `first line
|
||||
indented line
|
||||
double indented line
|
||||
back to single indent
|
||||
@@ -164,9 +163,9 @@ no indent
|
||||
double indent again
|
||||
triple indent
|
||||
back to single
|
||||
last line`;
|
||||
last line`
|
||||
|
||||
const diff = `--- original
|
||||
const diff = `--- original
|
||||
+++ modified
|
||||
@@ ... @@
|
||||
first line
|
||||
@@ -181,9 +180,9 @@ last line`;
|
||||
- triple indent
|
||||
+ hi there mate
|
||||
back to single
|
||||
last line`;
|
||||
last line`
|
||||
|
||||
const expected = `first line
|
||||
const expected = `first line
|
||||
indented line
|
||||
tab indented line
|
||||
new indented line
|
||||
@@ -194,23 +193,22 @@ no indent
|
||||
double indent again
|
||||
hi there mate
|
||||
back to single
|
||||
last line`;
|
||||
last line`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected);
|
||||
}
|
||||
});
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle high level edits', async () => {
|
||||
|
||||
const original = `def factorial(n):
|
||||
it("should handle high level edits", async () => {
|
||||
const original = `def factorial(n):
|
||||
if n == 0:
|
||||
return 1
|
||||
else:
|
||||
return n * factorial(n-1)`
|
||||
const diff = `@@ ... @@
|
||||
const diff = `@@ ... @@
|
||||
-def factorial(n):
|
||||
- if n == 0:
|
||||
- return 1
|
||||
@@ -222,21 +220,21 @@ last line`;
|
||||
+ else:
|
||||
+ return number * factorial(number-1)`
|
||||
|
||||
const expected = `def factorial(number):
|
||||
const expected = `def factorial(number):
|
||||
if number == 0:
|
||||
return 1
|
||||
else:
|
||||
return number * factorial(number-1)`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected);
|
||||
}
|
||||
});
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('it should handle very complex edits', async () => {
|
||||
const original = `//Initialize the array that will hold the primes
|
||||
it("it should handle very complex edits", async () => {
|
||||
const original = `//Initialize the array that will hold the primes
|
||||
var primeArray = [];
|
||||
/*Write a function that checks for primeness and
|
||||
pushes those values to t*he array*/
|
||||
@@ -269,7 +267,7 @@ for (var i = 2; primeArray.length < numPrimes; i++) {
|
||||
console.log(primeArray);
|
||||
`
|
||||
|
||||
const diff = `--- test_diff.js
|
||||
const diff = `--- test_diff.js
|
||||
+++ test_diff.js
|
||||
@@ ... @@
|
||||
-//Initialize the array that will hold the primes
|
||||
@@ -297,7 +295,7 @@ console.log(primeArray);
|
||||
}
|
||||
console.log(primeArray);`
|
||||
|
||||
const expected = `var primeArray = [];
|
||||
const expected = `var primeArray = [];
|
||||
function PrimeCheck(candidate){
|
||||
isPrime = true;
|
||||
for(var i = 2; i < candidate && isPrime; i++){
|
||||
@@ -320,58 +318,57 @@ for (var i = 2; primeArray.length < numPrimes; i++) {
|
||||
}
|
||||
console.log(primeArray);
|
||||
`
|
||||
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected);
|
||||
}
|
||||
});
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
describe('error handling and edge cases', () => {
|
||||
it('should reject completely invalid diff format', async () => {
|
||||
const original = 'line1\nline2\nline3';
|
||||
const invalidDiff = 'this is not a diff at all';
|
||||
|
||||
const result = await strategy.applyDiff(original, invalidDiff);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
describe("error handling and edge cases", () => {
|
||||
it("should reject completely invalid diff format", async () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const invalidDiff = "this is not a diff at all"
|
||||
|
||||
it('should reject diff with invalid hunk format', async () => {
|
||||
const original = 'line1\nline2\nline3';
|
||||
const invalidHunkDiff = `--- a/file.txt
|
||||
const result = await strategy.applyDiff(original, invalidDiff)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject diff with invalid hunk format", async () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const invalidHunkDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
invalid hunk header
|
||||
line1
|
||||
-line2
|
||||
+new line`;
|
||||
|
||||
const result = await strategy.applyDiff(original, invalidHunkDiff);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
+new line`
|
||||
|
||||
it('should fail when diff tries to modify non-existent content', async () => {
|
||||
const original = 'line1\nline2\nline3';
|
||||
const nonMatchingDiff = `--- a/file.txt
|
||||
const result = await strategy.applyDiff(original, invalidHunkDiff)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("should fail when diff tries to modify non-existent content", async () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const nonMatchingDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
-nonexistent line
|
||||
+new line
|
||||
line3`;
|
||||
|
||||
const result = await strategy.applyDiff(original, nonMatchingDiff);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
line3`
|
||||
|
||||
it('should handle overlapping hunks', async () => {
|
||||
const original = `line1
|
||||
const result = await strategy.applyDiff(original, nonMatchingDiff)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle overlapping hunks", async () => {
|
||||
const original = `line1
|
||||
line2
|
||||
line3
|
||||
line4
|
||||
line5`;
|
||||
const overlappingDiff = `--- a/file.txt
|
||||
line5`
|
||||
const overlappingDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
@@ -384,19 +381,19 @@ line5`;
|
||||
-line3
|
||||
-line4
|
||||
+modified3and4
|
||||
line5`;
|
||||
|
||||
const result = await strategy.applyDiff(original, overlappingDiff);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
line5`
|
||||
|
||||
it('should handle empty lines modifications', async () => {
|
||||
const original = `line1
|
||||
const result = await strategy.applyDiff(original, overlappingDiff)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle empty lines modifications", async () => {
|
||||
const original = `line1
|
||||
|
||||
line3
|
||||
|
||||
line5`;
|
||||
const emptyLinesDiff = `--- a/file.txt
|
||||
line5`
|
||||
const emptyLinesDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
@@ -404,73 +401,73 @@ line5`;
|
||||
-line3
|
||||
+line3modified
|
||||
|
||||
line5`;
|
||||
|
||||
const result = await strategy.applyDiff(original, emptyLinesDiff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
line5`
|
||||
|
||||
const result = await strategy.applyDiff(original, emptyLinesDiff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`line1
|
||||
|
||||
line3modified
|
||||
|
||||
line5`);
|
||||
}
|
||||
});
|
||||
line5`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle mixed line endings in diff', async () => {
|
||||
const original = 'line1\r\nline2\nline3\r\n';
|
||||
const mixedEndingsDiff = `--- a/file.txt
|
||||
it("should handle mixed line endings in diff", async () => {
|
||||
const original = "line1\r\nline2\nline3\r\n"
|
||||
const mixedEndingsDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
line1\r
|
||||
-line2
|
||||
+modified2\r
|
||||
line3`;
|
||||
|
||||
const result = await strategy.applyDiff(original, mixedEndingsDiff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe('line1\r\nmodified2\r\nline3\r\n');
|
||||
}
|
||||
});
|
||||
line3`
|
||||
|
||||
it('should handle partial line modifications', async () => {
|
||||
const original = 'const value = oldValue + 123;';
|
||||
const partialDiff = `--- a/file.txt
|
||||
const result = await strategy.applyDiff(original, mixedEndingsDiff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe("line1\r\nmodified2\r\nline3\r\n")
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle partial line modifications", async () => {
|
||||
const original = "const value = oldValue + 123;"
|
||||
const partialDiff = `--- a/file.txt
|
||||
+++ b/file.txt
|
||||
@@ ... @@
|
||||
-const value = oldValue + 123;
|
||||
+const value = newValue + 123;`;
|
||||
|
||||
const result = await strategy.applyDiff(original, partialDiff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe('const value = newValue + 123;');
|
||||
}
|
||||
});
|
||||
+const value = newValue + 123;`
|
||||
|
||||
it('should handle slightly malformed but recoverable diff', async () => {
|
||||
const original = 'line1\nline2\nline3';
|
||||
// Missing space after --- and +++
|
||||
const slightlyBadDiff = `---a/file.txt
|
||||
const result = await strategy.applyDiff(original, partialDiff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe("const value = newValue + 123;")
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle slightly malformed but recoverable diff", async () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
// Missing space after --- and +++
|
||||
const slightlyBadDiff = `---a/file.txt
|
||||
+++b/file.txt
|
||||
@@ ... @@
|
||||
line1
|
||||
-line2
|
||||
+new line
|
||||
line3`;
|
||||
|
||||
const result = await strategy.applyDiff(original, slightlyBadDiff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe('line1\nnew line\nline3');
|
||||
}
|
||||
});
|
||||
});
|
||||
line3`
|
||||
|
||||
describe('similar code sections', () => {
|
||||
it('should correctly modify the right section when similar code exists', async () => {
|
||||
const original = `function add(a, b) {
|
||||
const result = await strategy.applyDiff(original, slightlyBadDiff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe("line1\nnew line\nline3")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("similar code sections", () => {
|
||||
it("should correctly modify the right section when similar code exists", async () => {
|
||||
const original = `function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
@@ -480,20 +477,20 @@ function subtract(a, b) {
|
||||
|
||||
function multiply(a, b) {
|
||||
return a + b; // Bug here
|
||||
}`;
|
||||
}`
|
||||
|
||||
const diff = `--- a/math.js
|
||||
const diff = `--- a/math.js
|
||||
+++ b/math.js
|
||||
@@ ... @@
|
||||
function multiply(a, b) {
|
||||
- return a + b; // Bug here
|
||||
+ return a * b;
|
||||
}`;
|
||||
}`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function add(a, b) {
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
@@ -503,12 +500,12 @@ function subtract(a, b) {
|
||||
|
||||
function multiply(a, b) {
|
||||
return a * b;
|
||||
}`);
|
||||
}
|
||||
});
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle multiple similar sections with correct context', async () => {
|
||||
const original = `if (condition) {
|
||||
it("should handle multiple similar sections with correct context", async () => {
|
||||
const original = `if (condition) {
|
||||
doSomething();
|
||||
doSomething();
|
||||
doSomething();
|
||||
@@ -518,9 +515,9 @@ if (otherCondition) {
|
||||
doSomething();
|
||||
doSomething();
|
||||
doSomething();
|
||||
}`;
|
||||
}`
|
||||
|
||||
const diff = `--- a/file.js
|
||||
const diff = `--- a/file.js
|
||||
+++ b/file.js
|
||||
@@ ... @@
|
||||
if (otherCondition) {
|
||||
@@ -528,12 +525,12 @@ if (otherCondition) {
|
||||
- doSomething();
|
||||
+ doSomethingElse();
|
||||
doSomething();
|
||||
}`;
|
||||
}`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`if (condition) {
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`if (condition) {
|
||||
doSomething();
|
||||
doSomething();
|
||||
doSomething();
|
||||
@@ -543,14 +540,14 @@ if (otherCondition) {
|
||||
doSomething();
|
||||
doSomethingElse();
|
||||
doSomething();
|
||||
}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('hunk splitting', () => {
|
||||
it('should handle large diffs with multiple non-contiguous changes', async () => {
|
||||
const original = `import { readFile } from 'fs';
|
||||
describe("hunk splitting", () => {
|
||||
it("should handle large diffs with multiple non-contiguous changes", async () => {
|
||||
const original = `import { readFile } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Logger } from './logger';
|
||||
|
||||
@@ -595,9 +592,9 @@ export {
|
||||
validateInput,
|
||||
writeOutput,
|
||||
parseConfig
|
||||
};`;
|
||||
};`
|
||||
|
||||
const diff = `--- a/file.ts
|
||||
const diff = `--- a/file.ts
|
||||
+++ b/file.ts
|
||||
@@ ... @@
|
||||
-import { readFile } from 'fs';
|
||||
@@ -672,9 +669,9 @@ export {
|
||||
- parseConfig
|
||||
+ parseConfig,
|
||||
+ type Config
|
||||
};`;
|
||||
};`
|
||||
|
||||
const expected = `import { readFile, writeFile } from 'fs';
|
||||
const expected = `import { readFile, writeFile } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Logger } from './utils/logger';
|
||||
import { Config } from './types';
|
||||
@@ -727,13 +724,13 @@ export {
|
||||
writeOutput,
|
||||
parseConfig,
|
||||
type Config
|
||||
};`;
|
||||
};`
|
||||
|
||||
const result = await strategy.applyDiff(original, diff);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const result = await strategy.applyDiff(original, diff)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,27 @@
|
||||
import { UnifiedDiffStrategy } from '../unified'
|
||||
import { UnifiedDiffStrategy } from "../unified"
|
||||
|
||||
describe('UnifiedDiffStrategy', () => {
|
||||
let strategy: UnifiedDiffStrategy
|
||||
describe("UnifiedDiffStrategy", () => {
|
||||
let strategy: UnifiedDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new UnifiedDiffStrategy()
|
||||
})
|
||||
beforeEach(() => {
|
||||
strategy = new UnifiedDiffStrategy()
|
||||
})
|
||||
|
||||
describe('getToolDescription', () => {
|
||||
it('should return tool description with correct cwd', () => {
|
||||
const cwd = '/test/path'
|
||||
const description = strategy.getToolDescription(cwd)
|
||||
|
||||
expect(description).toContain('apply_diff')
|
||||
expect(description).toContain(cwd)
|
||||
expect(description).toContain('Parameters:')
|
||||
expect(description).toContain('Format Requirements:')
|
||||
})
|
||||
})
|
||||
describe("getToolDescription", () => {
|
||||
it("should return tool description with correct cwd", () => {
|
||||
const cwd = "/test/path"
|
||||
const description = strategy.getToolDescription({ cwd })
|
||||
|
||||
describe('applyDiff', () => {
|
||||
it('should successfully apply a function modification diff', async () => {
|
||||
const originalContent = `import { Logger } from '../logger';
|
||||
expect(description).toContain("apply_diff")
|
||||
expect(description).toContain(cwd)
|
||||
expect(description).toContain("Parameters:")
|
||||
expect(description).toContain("Format Requirements:")
|
||||
})
|
||||
})
|
||||
|
||||
describe("applyDiff", () => {
|
||||
it("should successfully apply a function modification diff", async () => {
|
||||
const originalContent = `import { Logger } from '../logger';
|
||||
|
||||
function calculateTotal(items: number[]): number {
|
||||
return items.reduce((sum, item) => {
|
||||
@@ -31,7 +31,7 @@ function calculateTotal(items: number[]): number {
|
||||
|
||||
export { calculateTotal };`
|
||||
|
||||
const diffContent = `--- src/utils/helper.ts
|
||||
const diffContent = `--- src/utils/helper.ts
|
||||
+++ src/utils/helper.ts
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Logger } from '../logger';
|
||||
@@ -47,7 +47,7 @@ export { calculateTotal };`
|
||||
|
||||
export { calculateTotal };`
|
||||
|
||||
const expected = `import { Logger } from '../logger';
|
||||
const expected = `import { Logger } from '../logger';
|
||||
|
||||
function calculateTotal(items: number[]): number {
|
||||
const total = items.reduce((sum, item) => {
|
||||
@@ -58,21 +58,21 @@ function calculateTotal(items: number[]): number {
|
||||
|
||||
export { calculateTotal };`
|
||||
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully apply a diff adding a new method', async () => {
|
||||
const originalContent = `class Calculator {
|
||||
it("should successfully apply a diff adding a new method", async () => {
|
||||
const originalContent = `class Calculator {
|
||||
add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
}`
|
||||
|
||||
const diffContent = `--- src/Calculator.ts
|
||||
const diffContent = `--- src/Calculator.ts
|
||||
+++ src/Calculator.ts
|
||||
@@ -1,5 +1,9 @@
|
||||
class Calculator {
|
||||
@@ -85,7 +85,7 @@ export { calculateTotal };`
|
||||
+ }
|
||||
}`
|
||||
|
||||
const expected = `class Calculator {
|
||||
const expected = `class Calculator {
|
||||
add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
@@ -95,15 +95,15 @@ export { calculateTotal };`
|
||||
}
|
||||
}`
|
||||
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully apply a diff modifying imports', async () => {
|
||||
const originalContent = `import { useState } from 'react';
|
||||
it("should successfully apply a diff modifying imports", async () => {
|
||||
const originalContent = `import { useState } from 'react';
|
||||
import { Button } from './components';
|
||||
|
||||
function App() {
|
||||
@@ -111,7 +111,7 @@ function App() {
|
||||
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
||||
}`
|
||||
|
||||
const diffContent = `--- src/App.tsx
|
||||
const diffContent = `--- src/App.tsx
|
||||
+++ src/App.tsx
|
||||
@@ -1,7 +1,8 @@
|
||||
-import { useState } from 'react';
|
||||
@@ -124,7 +124,7 @@ function App() {
|
||||
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
||||
}`
|
||||
|
||||
const expected = `import { useState, useEffect } from 'react';
|
||||
const expected = `import { useState, useEffect } from 'react';
|
||||
import { Button } from './components';
|
||||
|
||||
function App() {
|
||||
@@ -132,16 +132,16 @@ function App() {
|
||||
useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
|
||||
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
||||
}`
|
||||
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully apply a diff with multiple hunks', async () => {
|
||||
const originalContent = `import { readFile, writeFile } from 'fs';
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it("should successfully apply a diff with multiple hunks", async () => {
|
||||
const originalContent = `import { readFile, writeFile } from 'fs';
|
||||
|
||||
function processFile(path: string) {
|
||||
readFile(path, 'utf8', (err, data) => {
|
||||
@@ -155,7 +155,7 @@ function processFile(path: string) {
|
||||
|
||||
export { processFile };`
|
||||
|
||||
const diffContent = `--- src/file-processor.ts
|
||||
const diffContent = `--- src/file-processor.ts
|
||||
+++ src/file-processor.ts
|
||||
@@ -1,12 +1,14 @@
|
||||
-import { readFile, writeFile } from 'fs';
|
||||
@@ -182,7 +182,7 @@ export { processFile };`
|
||||
|
||||
export { processFile };`
|
||||
|
||||
const expected = `import { promises as fs } from 'fs';
|
||||
const expected = `import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
async function processFile(path: string) {
|
||||
@@ -198,32 +198,31 @@ async function processFile(path: string) {
|
||||
|
||||
export { processFile };`
|
||||
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle empty original content', async () => {
|
||||
const originalContent = ''
|
||||
const diffContent = `--- empty.ts
|
||||
it("should handle empty original content", async () => {
|
||||
const originalContent = ""
|
||||
const diffContent = `--- empty.ts
|
||||
+++ empty.ts
|
||||
@@ -0,0 +1,3 @@
|
||||
+export function greet(name: string): string {
|
||||
+ return \`Hello, \${name}!\`;
|
||||
+}`
|
||||
|
||||
const expected = `export function greet(name: string): string {
|
||||
const expected = `export function greet(name: string): string {
|
||||
return \`Hello, \${name}!\`;
|
||||
}\n`
|
||||
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -265,8 +265,8 @@ describe("applyGitFallback", () => {
|
||||
{ type: "context", content: "line1", indent: "" },
|
||||
{ type: "remove", content: "line2", indent: "" },
|
||||
{ type: "add", content: "new line2", indent: "" },
|
||||
{ type: "context", content: "line3", indent: "" }
|
||||
]
|
||||
{ type: "context", content: "line3", indent: "" },
|
||||
],
|
||||
} as Hunk
|
||||
|
||||
const content = ["line1", "line2", "line3"]
|
||||
@@ -281,8 +281,8 @@ describe("applyGitFallback", () => {
|
||||
const hunk = {
|
||||
changes: [
|
||||
{ type: "context", content: "nonexistent", indent: "" },
|
||||
{ type: "add", content: "new line", indent: "" }
|
||||
]
|
||||
{ type: "add", content: "new line", indent: "" },
|
||||
],
|
||||
} as Hunk
|
||||
|
||||
const content = ["line1", "line2", "line3"]
|
||||
|
||||
@@ -3,7 +3,7 @@ import { findAnchorMatch, findExactMatch, findSimilarityMatch, findLevenshteinMa
|
||||
type SearchStrategy = (
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex?: number
|
||||
startIndex?: number,
|
||||
) => {
|
||||
index: number
|
||||
confidence: number
|
||||
@@ -11,141 +11,141 @@ type SearchStrategy = (
|
||||
}
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: "should return no match if the search string is not found",
|
||||
searchStr: "not found",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match if the search string is found",
|
||||
searchStr: "line2",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with correct index when startIndex is provided",
|
||||
searchStr: "line3",
|
||||
content: ["line1", "line2", "line3", "line4", "line3"],
|
||||
startIndex: 3,
|
||||
expected: { index: 4, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if there are more lines in content",
|
||||
searchStr: "line2",
|
||||
content: ["line1", "line2", "line3", "line4", "line5"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the search string is at the beginning of the content",
|
||||
searchStr: "line1",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 0, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the search string is at the end of the content",
|
||||
searchStr: "line3",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match for a multi-line search string",
|
||||
searchStr: "line2\nline3",
|
||||
content: ["line1", "line2", "line3", "line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if a multi-line search string is not found",
|
||||
searchStr: "line2\nline4",
|
||||
content: ["line1", "line2", "line3", "line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with indentation",
|
||||
searchStr: " line2",
|
||||
content: ["line1", " line2", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with more complex indentation",
|
||||
searchStr: " line3",
|
||||
content: [" line1", " line2", " line3", " line4"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed indentation",
|
||||
searchStr: "\tline2",
|
||||
content: [" line1", "\tline2", " line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed indentation and multi-line",
|
||||
searchStr: " line2\n\tline3",
|
||||
content: ["line1", " line2", "\tline3", " line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if mixed indentation and multi-line is not found",
|
||||
searchStr: " line2\n line4",
|
||||
content: ["line1", " line2", "\tline3", " line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with leading and trailing spaces",
|
||||
searchStr: " line2 ",
|
||||
content: ["line1", " line2 ", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with leading and trailing tabs",
|
||||
searchStr: "\tline2\t",
|
||||
content: ["line1", "\tline2\t", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed leading and trailing spaces and tabs",
|
||||
searchStr: " \tline2\t ",
|
||||
content: ["line1", " \tline2\t ", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
|
||||
searchStr: " \tline2\t \n line3 ",
|
||||
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
|
||||
searchStr: " \tline2\t \n line4 ",
|
||||
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if the search string is not found",
|
||||
searchStr: "not found",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match if the search string is found",
|
||||
searchStr: "line2",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with correct index when startIndex is provided",
|
||||
searchStr: "line3",
|
||||
content: ["line1", "line2", "line3", "line4", "line3"],
|
||||
startIndex: 3,
|
||||
expected: { index: 4, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if there are more lines in content",
|
||||
searchStr: "line2",
|
||||
content: ["line1", "line2", "line3", "line4", "line5"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the search string is at the beginning of the content",
|
||||
searchStr: "line1",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 0, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the search string is at the end of the content",
|
||||
searchStr: "line3",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match for a multi-line search string",
|
||||
searchStr: "line2\nline3",
|
||||
content: ["line1", "line2", "line3", "line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if a multi-line search string is not found",
|
||||
searchStr: "line2\nline4",
|
||||
content: ["line1", "line2", "line3", "line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with indentation",
|
||||
searchStr: " line2",
|
||||
content: ["line1", " line2", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with more complex indentation",
|
||||
searchStr: " line3",
|
||||
content: [" line1", " line2", " line3", " line4"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed indentation",
|
||||
searchStr: "\tline2",
|
||||
content: [" line1", "\tline2", " line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed indentation and multi-line",
|
||||
searchStr: " line2\n\tline3",
|
||||
content: ["line1", " line2", "\tline3", " line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if mixed indentation and multi-line is not found",
|
||||
searchStr: " line2\n line4",
|
||||
content: ["line1", " line2", "\tline3", " line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with leading and trailing spaces",
|
||||
searchStr: " line2 ",
|
||||
content: ["line1", " line2 ", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with leading and trailing tabs",
|
||||
searchStr: "\tline2\t",
|
||||
content: ["line1", "\tline2\t", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed leading and trailing spaces and tabs",
|
||||
searchStr: " \tline2\t ",
|
||||
content: ["line1", " \tline2\t ", "line3"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
|
||||
searchStr: " \tline2\t \n line3 ",
|
||||
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||
expected: { index: 1, confidence: 1 },
|
||||
strategies: ["exact", "similarity", "levenshtein"],
|
||||
},
|
||||
{
|
||||
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
|
||||
searchStr: " \tline2\t \n line4 ",
|
||||
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
strategies: ["exact", "similarity"],
|
||||
},
|
||||
]
|
||||
|
||||
describe("findExactMatch", () => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
if (!strategies?.includes("exact")) {
|
||||
return
|
||||
}
|
||||
it(name, () => {
|
||||
it(name, () => {
|
||||
const result = findExactMatch(searchStr, content, startIndex)
|
||||
expect(result.index).toBe(expected.index)
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||
@@ -155,16 +155,16 @@ describe("findExactMatch", () => {
|
||||
})
|
||||
|
||||
describe("findAnchorMatch", () => {
|
||||
const anchorTestCases = [
|
||||
{
|
||||
name: "should return no match if no anchors are found",
|
||||
searchStr: " \n \n ",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
{
|
||||
name: "should return no match if anchor positions cannot be validated",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
const anchorTestCases = [
|
||||
{
|
||||
name: "should return no match if no anchors are found",
|
||||
searchStr: " \n \n ",
|
||||
content: ["line1", "line2", "line3"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
{
|
||||
name: "should return no match if anchor positions cannot be validated",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: [
|
||||
"different line 1",
|
||||
"different line 2",
|
||||
@@ -173,24 +173,24 @@ describe("findAnchorMatch", () => {
|
||||
"context line 1",
|
||||
"context line 2",
|
||||
],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
{
|
||||
name: "should return a match if anchor positions can be validated",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match with correct index when startIndex is provided",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
|
||||
startIndex: 3,
|
||||
expected: { index: 3, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if there are more lines in content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
{
|
||||
name: "should return a match if anchor positions can be validated",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match with correct index when startIndex is provided",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
|
||||
startIndex: 3,
|
||||
expected: { index: 3, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if there are more lines in content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: [
|
||||
"line1",
|
||||
"line2",
|
||||
@@ -201,30 +201,30 @@ describe("findAnchorMatch", () => {
|
||||
"extra line 1",
|
||||
"extra line 2",
|
||||
],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the anchor is at the beginning of the content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["unique line", "context line 1", "context line 2", "line 6"],
|
||||
expected: { index: 0, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the anchor is at the end of the content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return no match if no valid anchor is found",
|
||||
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the anchor is at the beginning of the content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["unique line", "context line 1", "context line 2", "line 6"],
|
||||
expected: { index: 0, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return a match even if the anchor is at the end of the content",
|
||||
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
|
||||
expected: { index: 2, confidence: 1 },
|
||||
},
|
||||
{
|
||||
name: "should return no match if no valid anchor is found",
|
||||
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
|
||||
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
|
||||
expected: { index: -1, confidence: 0 },
|
||||
},
|
||||
]
|
||||
|
||||
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
|
||||
it(name, () => {
|
||||
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
|
||||
it(name, () => {
|
||||
const result = findAnchorMatch(searchStr, content, startIndex)
|
||||
expect(result.index).toBe(expected.index)
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||
@@ -234,11 +234,11 @@ describe("findAnchorMatch", () => {
|
||||
})
|
||||
|
||||
describe("findSimilarityMatch", () => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
if (!strategies?.includes("similarity")) {
|
||||
return
|
||||
}
|
||||
it(name, () => {
|
||||
it(name, () => {
|
||||
const result = findSimilarityMatch(searchStr, content, startIndex)
|
||||
expect(result.index).toBe(expected.index)
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||
@@ -248,11 +248,11 @@ describe("findSimilarityMatch", () => {
|
||||
})
|
||||
|
||||
describe("findLevenshteinMatch", () => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||
if (!strategies?.includes("levenshtein")) {
|
||||
return
|
||||
}
|
||||
it(name, () => {
|
||||
it(name, () => {
|
||||
const result = findLevenshteinMatch(searchStr, content, startIndex)
|
||||
expect(result.index).toBe(expected.index)
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||
|
||||
@@ -18,7 +18,7 @@ function inferIndentation(line: string, contextLines: string[], previousIndent:
|
||||
const contextLine = contextLines[0]
|
||||
if (contextLine) {
|
||||
const contextMatch = contextLine.match(/^(\s+)/)
|
||||
if (contextMatch) {
|
||||
if (contextMatch) {
|
||||
return contextMatch[1]
|
||||
}
|
||||
}
|
||||
@@ -28,19 +28,15 @@ function inferIndentation(line: string, contextLines: string[], previousIndent:
|
||||
}
|
||||
|
||||
// Context matching edit strategy
|
||||
export function applyContextMatching(
|
||||
hunk: Hunk,
|
||||
content: string[],
|
||||
matchPosition: number,
|
||||
): EditResult {
|
||||
if (matchPosition === -1) {
|
||||
export function applyContextMatching(hunk: Hunk, content: string[], matchPosition: number): EditResult {
|
||||
if (matchPosition === -1) {
|
||||
return { confidence: 0, result: content, strategy: "context" }
|
||||
}
|
||||
|
||||
const newResult = [...content.slice(0, matchPosition)]
|
||||
let sourceIndex = matchPosition
|
||||
|
||||
for (const change of hunk.changes) {
|
||||
for (const change of hunk.changes) {
|
||||
if (change.type === "context") {
|
||||
// Use the original line from content if available
|
||||
if (sourceIndex < content.length) {
|
||||
@@ -82,20 +78,16 @@ export function applyContextMatching(
|
||||
|
||||
const confidence = validateEditResult(hunk, afterText)
|
||||
|
||||
return {
|
||||
return {
|
||||
confidence,
|
||||
result: newResult,
|
||||
strategy: "context"
|
||||
strategy: "context",
|
||||
}
|
||||
}
|
||||
|
||||
// DMP edit strategy
|
||||
export function applyDMP(
|
||||
hunk: Hunk,
|
||||
content: string[],
|
||||
matchPosition: number,
|
||||
): EditResult {
|
||||
if (matchPosition === -1) {
|
||||
export function applyDMP(hunk: Hunk, content: string[], matchPosition: number): EditResult {
|
||||
if (matchPosition === -1) {
|
||||
return { confidence: 0, result: content, strategy: "dmp" }
|
||||
}
|
||||
|
||||
@@ -105,9 +97,9 @@ export function applyDMP(
|
||||
const beforeLineCount = hunk.changes
|
||||
.filter((change) => change.type === "context" || change.type === "remove")
|
||||
.reduce((count, change) => count + change.content.split("\n").length, 0)
|
||||
|
||||
// Build BEFORE block (context + removals)
|
||||
const beforeLines = hunk.changes
|
||||
|
||||
// Build BEFORE block (context + removals)
|
||||
const beforeLines = hunk.changes
|
||||
.filter((change) => change.type === "context" || change.type === "remove")
|
||||
.map((change) => {
|
||||
if (change.originalLine) {
|
||||
@@ -115,9 +107,9 @@ export function applyDMP(
|
||||
}
|
||||
return change.indent ? change.indent + change.content : change.content
|
||||
})
|
||||
|
||||
// Build AFTER block (context + additions)
|
||||
const afterLines = hunk.changes
|
||||
|
||||
// Build AFTER block (context + additions)
|
||||
const afterLines = hunk.changes
|
||||
.filter((change) => change.type === "context" || change.type === "add")
|
||||
.map((change) => {
|
||||
if (change.originalLine) {
|
||||
@@ -139,17 +131,17 @@ export function applyDMP(
|
||||
const patchedLines = patchedText.split("\n")
|
||||
|
||||
// Construct final result
|
||||
const newResult = [
|
||||
...content.slice(0, matchPosition),
|
||||
...patchedLines,
|
||||
const newResult = [
|
||||
...content.slice(0, matchPosition),
|
||||
...patchedLines,
|
||||
...content.slice(matchPosition + beforeLineCount),
|
||||
]
|
||||
|
||||
|
||||
const confidence = validateEditResult(hunk, patchedText)
|
||||
|
||||
return {
|
||||
|
||||
return {
|
||||
confidence,
|
||||
result: newResult,
|
||||
result: newResult,
|
||||
strategy: "dmp",
|
||||
}
|
||||
}
|
||||
@@ -171,7 +163,7 @@ export async function applyGitFallback(hunk: Hunk, content: string[]): Promise<E
|
||||
const searchLines = hunk.changes
|
||||
.filter((change) => change.type === "context" || change.type === "remove")
|
||||
.map((change) => change.originalLine || change.indent + change.content)
|
||||
|
||||
|
||||
const replaceLines = hunk.changes
|
||||
.filter((change) => change.type === "context" || change.type === "add")
|
||||
.map((change) => change.originalLine || change.indent + change.content)
|
||||
@@ -272,16 +264,16 @@ export async function applyGitFallback(hunk: Hunk, content: string[]): Promise<E
|
||||
|
||||
// Main edit function that tries strategies sequentially
|
||||
export async function applyEdit(
|
||||
hunk: Hunk,
|
||||
content: string[],
|
||||
matchPosition: number,
|
||||
hunk: Hunk,
|
||||
content: string[],
|
||||
matchPosition: number,
|
||||
confidence: number,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): Promise<EditResult> {
|
||||
// Don't attempt regular edits if confidence is too low
|
||||
if (confidence < confidenceThreshold) {
|
||||
console.log(
|
||||
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`
|
||||
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`,
|
||||
)
|
||||
return applyGitFallback(hunk, content)
|
||||
}
|
||||
|
||||
@@ -164,8 +164,8 @@ Generate a unified diff that can be cleanly applied to modify code files.
|
||||
\`\`\`
|
||||
|
||||
Parameters:
|
||||
- path: (required) File path relative to ${cwd}
|
||||
- diff: (required) Unified diff content
|
||||
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
|
||||
- diff: (required) The diff content in unified format to apply to the file.
|
||||
|
||||
Usage:
|
||||
<apply_diff>
|
||||
@@ -233,7 +233,7 @@ Your diff here
|
||||
originalContent: string,
|
||||
diffContent: string,
|
||||
startLine?: number,
|
||||
endLine?: number
|
||||
endLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
const parsedDiff = this.parseUnifiedDiff(diffContent)
|
||||
const originalLines = originalContent.split("\n")
|
||||
@@ -271,7 +271,7 @@ Your diff here
|
||||
subHunkResult,
|
||||
subSearchResult.index,
|
||||
subSearchResult.confidence,
|
||||
this.confidenceThreshold
|
||||
this.confidenceThreshold,
|
||||
)
|
||||
if (subEditResult.confidence >= this.confidenceThreshold) {
|
||||
subHunkResult = subEditResult.result
|
||||
@@ -293,12 +293,12 @@ Your diff here
|
||||
const contextRatio = contextLines / totalLines
|
||||
|
||||
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
|
||||
confidence * 100
|
||||
confidence * 100,
|
||||
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
|
||||
errorMsg += "Debug Info:\n"
|
||||
errorMsg += `- Search Strategy Used: ${strategy}\n`
|
||||
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
|
||||
contextRatio * 100
|
||||
contextRatio * 100,
|
||||
)}%)\n`
|
||||
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
|
||||
|
||||
@@ -330,7 +330,7 @@ Your diff here
|
||||
} else {
|
||||
// Edit failure - likely due to content mismatch
|
||||
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
|
||||
editResult.confidence * 100
|
||||
editResult.confidence * 100,
|
||||
)}% confidence)\n\n`
|
||||
errorMsg += "Debug Info:\n"
|
||||
errorMsg += "- The location was found but the content didn't match exactly\n"
|
||||
|
||||
@@ -69,26 +69,26 @@ export function getDMPSimilarity(original: string, modified: string): number {
|
||||
export function validateEditResult(hunk: Hunk, result: string): number {
|
||||
// Build the expected text from the hunk
|
||||
const expectedText = hunk.changes
|
||||
.filter(change => change.type === "context" || change.type === "add")
|
||||
.map(change => change.indent ? change.indent + change.content : change.content)
|
||||
.join("\n");
|
||||
.filter((change) => change.type === "context" || change.type === "add")
|
||||
.map((change) => (change.indent ? change.indent + change.content : change.content))
|
||||
.join("\n")
|
||||
|
||||
// Calculate similarity between the result and expected text
|
||||
const similarity = getDMPSimilarity(expectedText, result);
|
||||
const similarity = getDMPSimilarity(expectedText, result)
|
||||
|
||||
// If the result is unchanged from original, return low confidence
|
||||
const originalText = hunk.changes
|
||||
.filter(change => change.type === "context" || change.type === "remove")
|
||||
.map(change => change.indent ? change.indent + change.content : change.content)
|
||||
.join("\n");
|
||||
.filter((change) => change.type === "context" || change.type === "remove")
|
||||
.map((change) => (change.indent ? change.indent + change.content : change.content))
|
||||
.join("\n")
|
||||
|
||||
const originalSimilarity = getDMPSimilarity(originalText, result);
|
||||
const originalSimilarity = getDMPSimilarity(originalText, result)
|
||||
if (originalSimilarity > 0.97 && similarity !== 1) {
|
||||
return 0.8 * similarity; // Some confidence since we found the right location
|
||||
return 0.8 * similarity // Some confidence since we found the right location
|
||||
}
|
||||
|
||||
|
||||
// For partial matches, scale the confidence but keep it high if we're close
|
||||
return similarity;
|
||||
return similarity
|
||||
}
|
||||
|
||||
// Helper function to validate context lines against original content
|
||||
@@ -114,7 +114,7 @@ function validateContextLines(searchStr: string, content: string, confidenceThre
|
||||
function createOverlappingWindows(
|
||||
content: string[],
|
||||
searchSize: number,
|
||||
overlapSize: number = DEFAULT_OVERLAP_SIZE
|
||||
overlapSize: number = DEFAULT_OVERLAP_SIZE,
|
||||
): { window: string[]; startIndex: number }[] {
|
||||
const windows: { window: string[]; startIndex: number }[] = []
|
||||
|
||||
@@ -140,7 +140,7 @@ function createOverlappingWindows(
|
||||
// Helper function to combine overlapping matches
|
||||
function combineOverlappingMatches(
|
||||
matches: (SearchResult & { windowIndex: number })[],
|
||||
overlapSize: number = DEFAULT_OVERLAP_SIZE
|
||||
overlapSize: number = DEFAULT_OVERLAP_SIZE,
|
||||
): SearchResult[] {
|
||||
if (matches.length === 0) {
|
||||
return []
|
||||
@@ -162,7 +162,7 @@ function combineOverlappingMatches(
|
||||
(m) =>
|
||||
Math.abs(m.windowIndex - match.windowIndex) === 1 &&
|
||||
Math.abs(m.index - match.index) <= overlapSize &&
|
||||
!usedIndices.has(m.windowIndex)
|
||||
!usedIndices.has(m.windowIndex),
|
||||
)
|
||||
|
||||
if (overlapping.length > 0) {
|
||||
@@ -196,7 +196,7 @@ export function findExactMatch(
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex: number = 0,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): SearchResult {
|
||||
const searchLines = searchStr.split("\n")
|
||||
const windows = createOverlappingWindows(content.slice(startIndex), searchLines.length)
|
||||
@@ -210,7 +210,7 @@ export function findExactMatch(
|
||||
const matchedContent = windowData.window
|
||||
.slice(
|
||||
windowStr.slice(0, exactMatch).split("\n").length - 1,
|
||||
windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length
|
||||
windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length,
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
@@ -236,7 +236,7 @@ export function findSimilarityMatch(
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex: number = 0,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): SearchResult {
|
||||
const searchLines = searchStr.split("\n")
|
||||
let bestScore = 0
|
||||
@@ -269,7 +269,7 @@ export function findLevenshteinMatch(
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex: number = 0,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): SearchResult {
|
||||
const searchLines = searchStr.split("\n")
|
||||
const candidates = []
|
||||
@@ -324,7 +324,7 @@ export function findAnchorMatch(
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex: number = 0,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): SearchResult {
|
||||
const searchLines = searchStr.split("\n")
|
||||
const { first, last } = identifyAnchors(searchStr)
|
||||
@@ -391,7 +391,7 @@ export function findBestMatch(
|
||||
searchStr: string,
|
||||
content: string[],
|
||||
startIndex: number = 0,
|
||||
confidenceThreshold: number = 0.97
|
||||
confidenceThreshold: number = 0.97,
|
||||
): SearchResult {
|
||||
const strategies = [findExactMatch, findAnchorMatch, findSimilarityMatch, findLevenshteinMatch]
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
export type Change = {
|
||||
type: 'context' | 'add' | 'remove';
|
||||
content: string;
|
||||
indent: string;
|
||||
originalLine?: string;
|
||||
};
|
||||
type: "context" | "add" | "remove"
|
||||
content: string
|
||||
indent: string
|
||||
originalLine?: string
|
||||
}
|
||||
|
||||
export type Hunk = {
|
||||
changes: Change[];
|
||||
};
|
||||
changes: Change[]
|
||||
}
|
||||
|
||||
export type Diff = {
|
||||
hunks: Hunk[];
|
||||
};
|
||||
hunks: Hunk[]
|
||||
}
|
||||
|
||||
export type EditResult = {
|
||||
confidence: number;
|
||||
result: string[];
|
||||
strategy: string;
|
||||
};
|
||||
confidence: number
|
||||
result: string[]
|
||||
strategy: string
|
||||
}
|
||||
|
||||
@@ -1,72 +1,74 @@
|
||||
import { DiffStrategy, DiffResult } from "../types"
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
|
||||
const BUFFER_LINES = 20; // Number of extra context lines to show before and after matches
|
||||
const BUFFER_LINES = 20 // Number of extra context lines to show before and after matches
|
||||
|
||||
function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
const matrix: number[][] = []
|
||||
|
||||
// Initialize matrix
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i] = [i];
|
||||
}
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
// Initialize matrix
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i] = [i]
|
||||
}
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j
|
||||
}
|
||||
|
||||
// Fill matrix
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
if (a[i-1] === b[j-1]) {
|
||||
matrix[i][j] = matrix[i-1][j-1];
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i-1][j-1] + 1, // substitution
|
||||
matrix[i][j-1] + 1, // insertion
|
||||
matrix[i-1][j] + 1 // deletion
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fill matrix
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
if (a[i - 1] === b[j - 1]) {
|
||||
matrix[i][j] = matrix[i - 1][j - 1]
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j - 1] + 1, // substitution
|
||||
matrix[i][j - 1] + 1, // insertion
|
||||
matrix[i - 1][j] + 1, // deletion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
return matrix[a.length][b.length]
|
||||
}
|
||||
|
||||
function getSimilarity(original: string, search: string): number {
|
||||
if (search === '') {
|
||||
return 1;
|
||||
}
|
||||
if (search === "") {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const normalizedOriginal = normalizeStr(original);
|
||||
const normalizedSearch = normalizeStr(search);
|
||||
|
||||
if (normalizedOriginal === normalizedSearch) { return 1; }
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch);
|
||||
|
||||
// Calculate similarity ratio (0 to 1, where 1 is exact match)
|
||||
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length);
|
||||
return 1 - (distance / maxLength);
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, " ").trim()
|
||||
|
||||
const normalizedOriginal = normalizeStr(original)
|
||||
const normalizedSearch = normalizeStr(search)
|
||||
|
||||
if (normalizedOriginal === normalizedSearch) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch)
|
||||
|
||||
// Calculate similarity ratio (0 to 1, where 1 is exact match)
|
||||
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
|
||||
return 1 - distance / maxLength
|
||||
}
|
||||
|
||||
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
||||
private fuzzyThreshold: number;
|
||||
private bufferLines: number;
|
||||
private fuzzyThreshold: number
|
||||
private bufferLines: number
|
||||
|
||||
constructor(fuzzyThreshold?: number, bufferLines?: number) {
|
||||
// Use provided threshold or default to exact matching (1.0)
|
||||
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
|
||||
// so we use it directly here
|
||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
|
||||
this.bufferLines = bufferLines ?? BUFFER_LINES;
|
||||
}
|
||||
constructor(fuzzyThreshold?: number, bufferLines?: number) {
|
||||
// Use provided threshold or default to exact matching (1.0)
|
||||
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
|
||||
// so we use it directly here
|
||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0
|
||||
this.bufferLines = bufferLines ?? BUFFER_LINES
|
||||
}
|
||||
|
||||
getToolDescription(cwd: string): string {
|
||||
return `## apply_diff
|
||||
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
|
||||
return `## apply_diff
|
||||
Description: Request to replace existing code using a search and replace block.
|
||||
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
|
||||
The tool will maintain proper indentation and formatting while making changes.
|
||||
@@ -76,7 +78,7 @@ If you're not confident in the exact content to search for, use the read_file to
|
||||
When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd})
|
||||
- diff: (required) The search/replace block defining the changes.
|
||||
- start_line: (required) The line number where the search block starts.
|
||||
- end_line: (required) The line number where the search block ends.
|
||||
@@ -125,193 +127,204 @@ Your search/replace content here
|
||||
<start_line>1</start_line>
|
||||
<end_line>5</end_line>
|
||||
</apply_diff>`
|
||||
}
|
||||
}
|
||||
|
||||
async applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult> {
|
||||
// Extract the search and replace blocks
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||
if (!match) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid diff format - missing required SEARCH/REPLACE sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers`
|
||||
};
|
||||
}
|
||||
async applyDiff(
|
||||
originalContent: string,
|
||||
diffContent: string,
|
||||
startLine?: number,
|
||||
endLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
// Extract the search and replace blocks
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/)
|
||||
if (!match) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid diff format - missing required SEARCH/REPLACE sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers`,
|
||||
}
|
||||
}
|
||||
|
||||
let [_, searchContent, replaceContent] = match;
|
||||
let [_, searchContent, replaceContent] = match
|
||||
|
||||
// Detect line ending from original content
|
||||
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
||||
// Detect line ending from original content
|
||||
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
||||
|
||||
// Strip line numbers from search and replace content if every line starts with a line number
|
||||
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
|
||||
searchContent = stripLineNumbers(searchContent);
|
||||
replaceContent = stripLineNumbers(replaceContent);
|
||||
}
|
||||
|
||||
// Split content into lines, handling both \n and \r\n
|
||||
const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
|
||||
const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
|
||||
const originalLines = originalContent.split(/\r?\n/);
|
||||
// Strip line numbers from search and replace content if every line starts with a line number
|
||||
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
|
||||
searchContent = stripLineNumbers(searchContent)
|
||||
replaceContent = stripLineNumbers(replaceContent)
|
||||
}
|
||||
|
||||
// Validate that empty search requires start line
|
||||
if (searchLines.length === 0 && !startLine) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`
|
||||
};
|
||||
}
|
||||
// Split content into lines, handling both \n and \r\n
|
||||
const searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
|
||||
const replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
|
||||
const originalLines = originalContent.split(/\r?\n/)
|
||||
|
||||
// Validate that empty search requires same start and end line
|
||||
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize search variables
|
||||
let matchIndex = -1;
|
||||
let bestMatchScore = 0;
|
||||
let bestMatchContent = "";
|
||||
const searchChunk = searchLines.join('\n');
|
||||
// Validate that empty search requires start line
|
||||
if (searchLines.length === 0 && !startLine) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`,
|
||||
}
|
||||
}
|
||||
|
||||
// Determine search bounds
|
||||
let searchStartIndex = 0;
|
||||
let searchEndIndex = originalLines.length;
|
||||
// Validate that empty search requires same start and end line
|
||||
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and handle line range if provided
|
||||
if (startLine && endLine) {
|
||||
// Convert to 0-based index
|
||||
const exactStartIndex = startLine - 1;
|
||||
const exactEndIndex = endLine - 1;
|
||||
// Initialize search variables
|
||||
let matchIndex = -1
|
||||
let bestMatchScore = 0
|
||||
let bestMatchContent = ""
|
||||
const searchChunk = searchLines.join("\n")
|
||||
|
||||
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}`,
|
||||
};
|
||||
}
|
||||
// Determine search bounds
|
||||
let searchStartIndex = 0
|
||||
let searchEndIndex = originalLines.length
|
||||
|
||||
// Try exact match first
|
||||
const originalChunk = originalLines.slice(exactStartIndex, exactEndIndex + 1).join('\n');
|
||||
const similarity = getSimilarity(originalChunk, searchChunk);
|
||||
if (similarity >= this.fuzzyThreshold) {
|
||||
matchIndex = exactStartIndex;
|
||||
bestMatchScore = similarity;
|
||||
bestMatchContent = originalChunk;
|
||||
} else {
|
||||
// Set bounds for buffered search
|
||||
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1));
|
||||
searchEndIndex = Math.min(originalLines.length, endLine + this.bufferLines);
|
||||
}
|
||||
}
|
||||
// Validate and handle line range if provided
|
||||
if (startLine && endLine) {
|
||||
// Convert to 0-based index
|
||||
const exactStartIndex = startLine - 1
|
||||
const exactEndIndex = endLine - 1
|
||||
|
||||
// If no match found yet, try middle-out search within bounds
|
||||
if (matchIndex === -1) {
|
||||
const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2);
|
||||
let leftIndex = midPoint;
|
||||
let rightIndex = midPoint + 1;
|
||||
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Search outward from the middle within bounds
|
||||
while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
|
||||
// Check left side if still in range
|
||||
if (leftIndex >= searchStartIndex) {
|
||||
const originalChunk = originalLines.slice(leftIndex, leftIndex + searchLines.length).join('\n');
|
||||
const similarity = getSimilarity(originalChunk, searchChunk);
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity;
|
||||
matchIndex = leftIndex;
|
||||
bestMatchContent = originalChunk;
|
||||
}
|
||||
leftIndex--;
|
||||
}
|
||||
// Try exact match first
|
||||
const originalChunk = originalLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity >= this.fuzzyThreshold) {
|
||||
matchIndex = exactStartIndex
|
||||
bestMatchScore = similarity
|
||||
bestMatchContent = originalChunk
|
||||
} else {
|
||||
// Set bounds for buffered search
|
||||
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
|
||||
searchEndIndex = Math.min(originalLines.length, endLine + this.bufferLines)
|
||||
}
|
||||
}
|
||||
|
||||
// Check right side if still in range
|
||||
if (rightIndex <= searchEndIndex - searchLines.length) {
|
||||
const originalChunk = originalLines.slice(rightIndex, rightIndex + searchLines.length).join('\n');
|
||||
const similarity = getSimilarity(originalChunk, searchChunk);
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity;
|
||||
matchIndex = rightIndex;
|
||||
bestMatchContent = originalChunk;
|
||||
}
|
||||
rightIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no match found yet, try middle-out search within bounds
|
||||
if (matchIndex === -1) {
|
||||
const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2)
|
||||
let leftIndex = midPoint
|
||||
let rightIndex = midPoint + 1
|
||||
|
||||
// Require similarity to meet threshold
|
||||
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
||||
const searchChunk = searchLines.join('\n');
|
||||
const originalContentSection = startLine !== undefined && endLine !== undefined
|
||||
? `\n\nOriginal Content:\n${addLineNumbers(
|
||||
originalLines.slice(
|
||||
Math.max(0, startLine - 1 - this.bufferLines),
|
||||
Math.min(originalLines.length, endLine + this.bufferLines)
|
||||
).join('\n'),
|
||||
Math.max(1, startLine - this.bufferLines)
|
||||
)}`
|
||||
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`;
|
||||
// Search outward from the middle within bounds
|
||||
while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
|
||||
// Check left side if still in range
|
||||
if (leftIndex >= searchStartIndex) {
|
||||
const originalChunk = originalLines.slice(leftIndex, leftIndex + searchLines.length).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity
|
||||
matchIndex = leftIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
leftIndex--
|
||||
}
|
||||
|
||||
const bestMatchSection = bestMatchContent
|
||||
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
||||
: `\n\nBest Match Found:\n(no match)`;
|
||||
// Check right side if still in range
|
||||
if (rightIndex <= searchEndIndex - searchLines.length) {
|
||||
const originalChunk = originalLines.slice(rightIndex, rightIndex + searchLines.length).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity
|
||||
matchIndex = rightIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
rightIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lineRange = startLine || endLine ?
|
||||
` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : '';
|
||||
return {
|
||||
success: false,
|
||||
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`
|
||||
};
|
||||
}
|
||||
// Require similarity to meet threshold
|
||||
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
||||
const searchChunk = searchLines.join("\n")
|
||||
const originalContentSection =
|
||||
startLine !== undefined && endLine !== undefined
|
||||
? `\n\nOriginal Content:\n${addLineNumbers(
|
||||
originalLines
|
||||
.slice(
|
||||
Math.max(0, startLine - 1 - this.bufferLines),
|
||||
Math.min(originalLines.length, endLine + this.bufferLines),
|
||||
)
|
||||
.join("\n"),
|
||||
Math.max(1, startLine - this.bufferLines),
|
||||
)}`
|
||||
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join("\n"))}`
|
||||
|
||||
// Get the matched lines from the original content
|
||||
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
|
||||
|
||||
// Get the exact indentation (preserving tabs/spaces) of each line
|
||||
const originalIndents = matchedLines.map(line => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
const bestMatchSection = bestMatchContent
|
||||
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
||||
: `\n\nBest Match Found:\n(no match)`
|
||||
|
||||
// Get the exact indentation of each line in the search block
|
||||
const searchIndents = searchLines.map(line => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
const lineRange =
|
||||
startLine || endLine
|
||||
? ` at ${startLine ? `start: ${startLine}` : "start"} to ${endLine ? `end: ${endLine}` : "end"}`
|
||||
: ""
|
||||
return {
|
||||
success: false,
|
||||
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : "start to end"}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||
// Get the matched line's exact indentation
|
||||
const matchedIndent = originalIndents[0] || '';
|
||||
|
||||
// Get the current line's indentation relative to the search content
|
||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
|
||||
const searchBaseIndent = searchIndents[0] || '';
|
||||
|
||||
// Calculate the relative indentation level
|
||||
const searchBaseLevel = searchBaseIndent.length;
|
||||
const currentLevel = currentIndent.length;
|
||||
const relativeLevel = currentLevel - searchBaseLevel;
|
||||
|
||||
// If relative level is negative, remove indentation from matched indent
|
||||
// If positive, add to matched indent
|
||||
const finalIndent = relativeLevel < 0
|
||||
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
|
||||
: matchedIndent + currentIndent.slice(searchBaseLevel);
|
||||
|
||||
return finalIndent + line.trim();
|
||||
});
|
||||
// Get the matched lines from the original content
|
||||
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length)
|
||||
|
||||
// Construct the final content
|
||||
const beforeMatch = originalLines.slice(0, matchIndex);
|
||||
const afterMatch = originalLines.slice(matchIndex + searchLines.length);
|
||||
|
||||
const finalContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding);
|
||||
return {
|
||||
success: true,
|
||||
content: finalContent
|
||||
};
|
||||
}
|
||||
}
|
||||
// Get the exact indentation (preserving tabs/spaces) of each line
|
||||
const originalIndents = matchedLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Get the exact indentation of each line in the search block
|
||||
const searchIndents = searchLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||
// Get the matched line's exact indentation
|
||||
const matchedIndent = originalIndents[0] || ""
|
||||
|
||||
// Get the current line's indentation relative to the search content
|
||||
const currentIndentMatch = line.match(/^[\t ]*/)
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
|
||||
const searchBaseIndent = searchIndents[0] || ""
|
||||
|
||||
// Calculate the relative indentation level
|
||||
const searchBaseLevel = searchBaseIndent.length
|
||||
const currentLevel = currentIndent.length
|
||||
const relativeLevel = currentLevel - searchBaseLevel
|
||||
|
||||
// If relative level is negative, remove indentation from matched indent
|
||||
// If positive, add to matched indent
|
||||
const finalIndent =
|
||||
relativeLevel < 0
|
||||
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
|
||||
: matchedIndent + currentIndent.slice(searchBaseLevel)
|
||||
|
||||
return finalIndent + line.trim()
|
||||
})
|
||||
|
||||
// Construct the final content
|
||||
const beforeMatch = originalLines.slice(0, matchIndex)
|
||||
const afterMatch = originalLines.slice(matchIndex + searchLines.length)
|
||||
|
||||
const finalContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding)
|
||||
return {
|
||||
success: true,
|
||||
content: finalContent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { applyPatch } from "diff"
|
||||
import { DiffStrategy, DiffResult } from "../types"
|
||||
|
||||
export class UnifiedDiffStrategy implements DiffStrategy {
|
||||
getToolDescription(cwd: string): string {
|
||||
return `## apply_diff
|
||||
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
|
||||
return `## apply_diff
|
||||
Description: Apply a unified diff to a file at the specified path. This tool is useful when you need to make specific modifications to a file based on a set of changes provided in unified diff format (diff -U3).
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd})
|
||||
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
|
||||
- diff: (required) The diff content in unified format to apply to the file.
|
||||
|
||||
Format Requirements:
|
||||
@@ -106,32 +106,32 @@ Usage:
|
||||
Your diff here
|
||||
</diff>
|
||||
</apply_diff>`
|
||||
}
|
||||
}
|
||||
|
||||
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
|
||||
try {
|
||||
const result = applyPatch(originalContent, diffContent)
|
||||
if (result === false) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to apply unified diff - patch rejected",
|
||||
details: {
|
||||
searchContent: diffContent
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
content: result
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Error applying unified diff: ${error.message}`,
|
||||
details: {
|
||||
searchContent: diffContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
|
||||
try {
|
||||
const result = applyPatch(originalContent, diffContent)
|
||||
if (result === false) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to apply unified diff - patch rejected",
|
||||
details: {
|
||||
searchContent: diffContent,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
content: result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Error applying unified diff: ${error.message}`,
|
||||
details: {
|
||||
searchContent: diffContent,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,35 @@
|
||||
* Interface for implementing different diff strategies
|
||||
*/
|
||||
|
||||
export type DiffResult =
|
||||
| { success: true; content: string }
|
||||
| { success: false; error: string; details?: {
|
||||
similarity?: number;
|
||||
threshold?: number;
|
||||
matchedRange?: { start: number; end: number };
|
||||
searchContent?: string;
|
||||
bestMatch?: string;
|
||||
}};
|
||||
export type DiffResult =
|
||||
| { success: true; content: string }
|
||||
| {
|
||||
success: false
|
||||
error: string
|
||||
details?: {
|
||||
similarity?: number
|
||||
threshold?: number
|
||||
matchedRange?: { start: number; end: number }
|
||||
searchContent?: string
|
||||
bestMatch?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiffStrategy {
|
||||
/**
|
||||
* Get the tool description for this diff strategy
|
||||
* @param cwd The current working directory
|
||||
* @returns The complete tool description including format requirements and examples
|
||||
*/
|
||||
getToolDescription(cwd: string): string
|
||||
/**
|
||||
* Get the tool description for this diff strategy
|
||||
* @param args The tool arguments including cwd and toolOptions
|
||||
* @returns The complete tool description including format requirements and examples
|
||||
*/
|
||||
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string
|
||||
|
||||
/**
|
||||
* Apply a diff to the original content
|
||||
* @param originalContent The original file content
|
||||
* @param diffContent The diff content in the strategy's format
|
||||
* @param startLine Optional line number where the search block starts. If not provided, searches the entire file.
|
||||
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
|
||||
* @returns A DiffResult object containing either the successful result or error details
|
||||
*/
|
||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult>
|
||||
}
|
||||
/**
|
||||
* Apply a diff to the original content
|
||||
* @param originalContent The original file content
|
||||
* @param diffContent The diff content in the strategy's format
|
||||
* @param startLine Optional line number where the search block starts. If not provided, searches the entire file.
|
||||
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
|
||||
* @returns A DiffResult object containing either the successful result or error details
|
||||
*/
|
||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// Create mock vscode module before importing anything
|
||||
const createMockUri = (scheme: string, path: string) => ({
|
||||
scheme,
|
||||
authority: '',
|
||||
authority: "",
|
||||
path,
|
||||
query: '',
|
||||
fragment: '',
|
||||
query: "",
|
||||
fragment: "",
|
||||
fsPath: path,
|
||||
with: jest.fn(),
|
||||
toString: () => path,
|
||||
toJSON: () => ({
|
||||
scheme,
|
||||
authority: '',
|
||||
authority: "",
|
||||
path,
|
||||
query: '',
|
||||
fragment: ''
|
||||
})
|
||||
query: "",
|
||||
fragment: "",
|
||||
}),
|
||||
})
|
||||
|
||||
const mockExecuteCommand = jest.fn()
|
||||
@@ -23,9 +23,11 @@ const mockShowErrorMessage = jest.fn()
|
||||
|
||||
const mockVscode = {
|
||||
workspace: {
|
||||
workspaceFolders: [{
|
||||
uri: { fsPath: "/test/workspace" }
|
||||
}]
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
},
|
||||
],
|
||||
},
|
||||
window: {
|
||||
showErrorMessage: mockShowErrorMessage,
|
||||
@@ -34,17 +36,17 @@ const mockVscode = {
|
||||
createTextEditorDecorationType: jest.fn(),
|
||||
createOutputChannel: jest.fn(),
|
||||
createWebviewPanel: jest.fn(),
|
||||
activeTextEditor: undefined
|
||||
activeTextEditor: undefined,
|
||||
},
|
||||
commands: {
|
||||
executeCommand: mockExecuteCommand
|
||||
executeCommand: mockExecuteCommand,
|
||||
},
|
||||
env: {
|
||||
openExternal: mockOpenExternal
|
||||
openExternal: mockOpenExternal,
|
||||
},
|
||||
Uri: {
|
||||
parse: jest.fn((url: string) => createMockUri('https', url)),
|
||||
file: jest.fn((path: string) => createMockUri('file', path))
|
||||
parse: jest.fn((url: string) => createMockUri("https", url)),
|
||||
file: jest.fn((path: string) => createMockUri("file", path)),
|
||||
},
|
||||
Position: jest.fn(),
|
||||
Range: jest.fn(),
|
||||
@@ -54,12 +56,12 @@ const mockVscode = {
|
||||
Error: 0,
|
||||
Warning: 1,
|
||||
Information: 2,
|
||||
Hint: 3
|
||||
}
|
||||
Hint: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// Mock modules
|
||||
jest.mock('vscode', () => mockVscode)
|
||||
jest.mock("vscode", () => mockVscode)
|
||||
jest.mock("../../../services/browser/UrlContentFetcher")
|
||||
jest.mock("../../../utils/git")
|
||||
|
||||
@@ -74,7 +76,7 @@ describe("mentions", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
|
||||
// Create a mock instance with just the methods we need
|
||||
mockUrlContentFetcher = {
|
||||
launchBrowser: jest.fn().mockResolvedValue(undefined),
|
||||
@@ -94,14 +96,10 @@ Date: Mon Jan 5 23:50:06 2025 -0500
|
||||
Detailed commit message with multiple lines
|
||||
- Fixed parsing issue
|
||||
- Added tests`
|
||||
|
||||
|
||||
jest.mocked(git.getCommitInfo).mockResolvedValue(commitInfo)
|
||||
|
||||
const result = await parseMentions(
|
||||
`Check out this commit @${commitHash}`,
|
||||
mockCwd,
|
||||
mockUrlContentFetcher
|
||||
)
|
||||
const result = await parseMentions(`Check out this commit @${commitHash}`, mockCwd, mockUrlContentFetcher)
|
||||
|
||||
expect(result).toContain(`'${commitHash}' (see below for commit info)`)
|
||||
expect(result).toContain(`<git_commit hash="${commitHash}">`)
|
||||
@@ -111,14 +109,10 @@ Detailed commit message with multiple lines
|
||||
it("should handle errors fetching git info", async () => {
|
||||
const commitHash = "abc1234"
|
||||
const errorMessage = "Failed to get commit info"
|
||||
|
||||
|
||||
jest.mocked(git.getCommitInfo).mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
const result = await parseMentions(
|
||||
`Check out this commit @${commitHash}`,
|
||||
mockCwd,
|
||||
mockUrlContentFetcher
|
||||
)
|
||||
const result = await parseMentions(`Check out this commit @${commitHash}`, mockCwd, mockUrlContentFetcher)
|
||||
|
||||
expect(result).toContain(`'${commitHash}' (see below for commit info)`)
|
||||
expect(result).toContain(`<git_commit hash="${commitHash}">`)
|
||||
@@ -143,13 +137,15 @@ Detailed commit message with multiple lines
|
||||
const mockUri = mockVscode.Uri.parse(url)
|
||||
expect(mockOpenExternal).toHaveBeenCalled()
|
||||
const calledArg = mockOpenExternal.mock.calls[0][0]
|
||||
expect(calledArg).toEqual(expect.objectContaining({
|
||||
scheme: mockUri.scheme,
|
||||
authority: mockUri.authority,
|
||||
path: mockUri.path,
|
||||
query: mockUri.query,
|
||||
fragment: mockUri.fragment
|
||||
}))
|
||||
expect(calledArg).toEqual(
|
||||
expect.objectContaining({
|
||||
scheme: mockUri.scheme,
|
||||
authority: mockUri.authority,
|
||||
path: mockUri.path,
|
||||
query: mockUri.query,
|
||||
fragment: mockUri.fragment,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,32 +1,10 @@
|
||||
import { Mode } from './prompts/types'
|
||||
import { codeMode } from './prompts/system'
|
||||
import { CODE_ALLOWED_TOOLS, READONLY_ALLOWED_TOOLS, ToolName, ReadOnlyToolName } from './tool-lists'
|
||||
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig } from "../shared/modes"
|
||||
|
||||
// Extended tool type that includes 'unknown_tool' for testing
|
||||
export type TestToolName = ToolName | 'unknown_tool';
|
||||
|
||||
// Type guard to check if a tool is a valid tool
|
||||
function isValidTool(tool: TestToolName): tool is ToolName {
|
||||
return CODE_ALLOWED_TOOLS.includes(tool as ToolName);
|
||||
}
|
||||
|
||||
// Type guard to check if a tool is a read-only tool
|
||||
function isReadOnlyTool(tool: TestToolName): tool is ReadOnlyToolName {
|
||||
return READONLY_ALLOWED_TOOLS.includes(tool as ReadOnlyToolName);
|
||||
}
|
||||
|
||||
export function isToolAllowedForMode(toolName: TestToolName, mode: Mode): boolean {
|
||||
if (mode === codeMode) {
|
||||
return isValidTool(toolName);
|
||||
}
|
||||
// Both architect and ask modes use the same read-only tools
|
||||
return isReadOnlyTool(toolName);
|
||||
}
|
||||
export { isToolAllowedForMode }
|
||||
export type { TestToolName }
|
||||
|
||||
export function validateToolUse(toolName: TestToolName, mode: Mode): void {
|
||||
if (!isToolAllowedForMode(toolName, mode)) {
|
||||
throw new Error(
|
||||
`Tool "${toolName}" is not allowed in ${mode} mode.`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isToolAllowedForMode(toolName, mode)) {
|
||||
throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,139 +0,0 @@
|
||||
import { ARCHITECT_PROMPT } from '../architect'
|
||||
import { McpHub } from '../../../services/mcp/McpHub'
|
||||
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os'
|
||||
// Import path utils to get access to toPosix string extension
|
||||
import '../../../utils/path'
|
||||
|
||||
// Mock environment-specific values for consistent tests
|
||||
jest.mock('os', () => ({
|
||||
...jest.requireActual('os'),
|
||||
homedir: () => '/home/user'
|
||||
}))
|
||||
|
||||
jest.mock('default-shell', () => '/bin/bash')
|
||||
|
||||
jest.mock('os-name', () => () => 'Linux')
|
||||
|
||||
// Mock fs.readFile to return empty mcpServers config
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith('mcpSettings.json')) {
|
||||
return '{"mcpServers": {}}'
|
||||
}
|
||||
if (path.endsWith('.clinerules')) {
|
||||
return '# Test Rules\n1. First rule\n2. Second rule'
|
||||
}
|
||||
return ''
|
||||
}),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
|
||||
// Instead of extending McpHub, create a mock that implements just what we need
|
||||
const createMockMcpHub = (): McpHub => ({
|
||||
getServers: () => [],
|
||||
getMcpServersPath: async () => '/mock/mcp/path',
|
||||
getMcpSettingsFilePath: async () => '/mock/settings/path',
|
||||
dispose: async () => {},
|
||||
// Add other required public methods with no-op implementations
|
||||
restartConnection: async () => {},
|
||||
readResource: async () => ({ contents: [] }),
|
||||
callTool: async () => ({ content: [] }),
|
||||
toggleServerDisabled: async () => {},
|
||||
toggleToolAlwaysAllow: async () => {},
|
||||
isConnecting: false,
|
||||
connections: []
|
||||
} as unknown as McpHub)
|
||||
|
||||
describe('ARCHITECT_PROMPT', () => {
|
||||
let mockMcpHub: McpHub
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any McpHub instances
|
||||
if (mockMcpHub) {
|
||||
await mockMcpHub.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('should maintain consistent architect prompt', async () => {
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined // browserViewportSize
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include browser actions when supportsComputerUse is true', async () => {
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'1280x800'
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include MCP server info when mcpHub is provided', async () => {
|
||||
mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
mockMcpHub
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should explicitly handle undefined mcpHub', async () => {
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined, // explicitly undefined mcpHub
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle different browser viewport sizes', async () => {
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'900x600' // different viewport size
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include diff strategy tool description', async () => {
|
||||
const prompt = await ARCHITECT_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined,
|
||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
@@ -1,139 +0,0 @@
|
||||
import { ASK_PROMPT } from '../ask'
|
||||
import { McpHub } from '../../../services/mcp/McpHub'
|
||||
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os'
|
||||
// Import path utils to get access to toPosix string extension
|
||||
import '../../../utils/path'
|
||||
|
||||
// Mock environment-specific values for consistent tests
|
||||
jest.mock('os', () => ({
|
||||
...jest.requireActual('os'),
|
||||
homedir: () => '/home/user'
|
||||
}))
|
||||
|
||||
jest.mock('default-shell', () => '/bin/bash')
|
||||
|
||||
jest.mock('os-name', () => () => 'Linux')
|
||||
|
||||
// Mock fs.readFile to return empty mcpServers config
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith('mcpSettings.json')) {
|
||||
return '{"mcpServers": {}}'
|
||||
}
|
||||
if (path.endsWith('.clinerules')) {
|
||||
return '# Test Rules\n1. First rule\n2. Second rule'
|
||||
}
|
||||
return ''
|
||||
}),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
|
||||
// Instead of extending McpHub, create a mock that implements just what we need
|
||||
const createMockMcpHub = (): McpHub => ({
|
||||
getServers: () => [],
|
||||
getMcpServersPath: async () => '/mock/mcp/path',
|
||||
getMcpSettingsFilePath: async () => '/mock/settings/path',
|
||||
dispose: async () => {},
|
||||
// Add other required public methods with no-op implementations
|
||||
restartConnection: async () => {},
|
||||
readResource: async () => ({ contents: [] }),
|
||||
callTool: async () => ({ content: [] }),
|
||||
toggleServerDisabled: async () => {},
|
||||
toggleToolAlwaysAllow: async () => {},
|
||||
isConnecting: false,
|
||||
connections: []
|
||||
} as unknown as McpHub)
|
||||
|
||||
describe('ASK_PROMPT', () => {
|
||||
let mockMcpHub: McpHub
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any McpHub instances
|
||||
if (mockMcpHub) {
|
||||
await mockMcpHub.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('should maintain consistent ask prompt', async () => {
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined // browserViewportSize
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include browser actions when supportsComputerUse is true', async () => {
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'1280x800'
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include MCP server info when mcpHub is provided', async () => {
|
||||
mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
mockMcpHub
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should explicitly handle undefined mcpHub', async () => {
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined, // explicitly undefined mcpHub
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle different browser viewport sizes', async () => {
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'900x600' // different viewport size
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include diff strategy tool description', async () => {
|
||||
const prompt = await ASK_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined,
|
||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
@@ -1,320 +1,357 @@
|
||||
import { SYSTEM_PROMPT, addCustomInstructions } from '../system'
|
||||
import { McpHub } from '../../../services/mcp/McpHub'
|
||||
import { McpServer } from '../../../shared/mcp'
|
||||
import { ClineProvider } from '../../../core/webview/ClineProvider'
|
||||
import { SearchReplaceDiffStrategy } from '../../../core/diff/strategies/search-replace'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os'
|
||||
import { codeMode, askMode, architectMode } from '../modes'
|
||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../system"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { McpServer } from "../../../shared/mcp"
|
||||
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||
import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||
// Import path utils to get access to toPosix string extension
|
||||
import '../../../utils/path'
|
||||
import "../../../utils/path"
|
||||
|
||||
// Mock environment-specific values for consistent tests
|
||||
jest.mock('os', () => ({
|
||||
...jest.requireActual('os'),
|
||||
homedir: () => '/home/user'
|
||||
jest.mock("os", () => ({
|
||||
...jest.requireActual("os"),
|
||||
homedir: () => "/home/user",
|
||||
}))
|
||||
|
||||
jest.mock('default-shell', () => '/bin/bash')
|
||||
jest.mock("default-shell", () => "/bin/bash")
|
||||
|
||||
jest.mock('os-name', () => () => 'Linux')
|
||||
jest.mock("os-name", () => () => "Linux")
|
||||
|
||||
// Mock fs.readFile to return empty mcpServers config and mock rules files
|
||||
jest.mock('fs/promises', () => ({
|
||||
...jest.requireActual('fs/promises'),
|
||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith('mcpSettings.json')) {
|
||||
return '{"mcpServers": {}}'
|
||||
}
|
||||
if (path.endsWith('.clinerules-code')) {
|
||||
return '# Code Mode Rules\n1. Code specific rule'
|
||||
}
|
||||
if (path.endsWith('.clinerules-ask')) {
|
||||
return '# Ask Mode Rules\n1. Ask specific rule'
|
||||
}
|
||||
if (path.endsWith('.clinerules-architect')) {
|
||||
return '# Architect Mode Rules\n1. Architect specific rule'
|
||||
}
|
||||
if (path.endsWith('.clinerules')) {
|
||||
return '# Test Rules\n1. First rule\n2. Second rule'
|
||||
}
|
||||
return ''
|
||||
}),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined)
|
||||
jest.mock("fs/promises", () => ({
|
||||
...jest.requireActual("fs/promises"),
|
||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith("mcpSettings.json")) {
|
||||
return '{"mcpServers": {}}'
|
||||
}
|
||||
if (path.endsWith(".clinerules-code")) {
|
||||
return "# Code Mode Rules\n1. Code specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules-ask")) {
|
||||
return "# Ask Mode Rules\n1. Ask specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules-architect")) {
|
||||
return "# Architect Mode Rules\n1. Architect specific rule"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
}),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
// Create a minimal mock of ClineProvider
|
||||
const mockProvider = {
|
||||
ensureMcpServersDirectoryExists: async () => '/mock/mcp/path',
|
||||
ensureSettingsDirectoryExists: async () => '/mock/settings/path',
|
||||
postMessageToWebview: async () => {},
|
||||
context: {
|
||||
extension: {
|
||||
packageJSON: {
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
ensureMcpServersDirectoryExists: async () => "/mock/mcp/path",
|
||||
ensureSettingsDirectoryExists: async () => "/mock/settings/path",
|
||||
postMessageToWebview: async () => {},
|
||||
context: {
|
||||
extension: {
|
||||
packageJSON: {
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ClineProvider
|
||||
|
||||
// Instead of extending McpHub, create a mock that implements just what we need
|
||||
const createMockMcpHub = (): McpHub => ({
|
||||
getServers: () => [],
|
||||
getMcpServersPath: async () => '/mock/mcp/path',
|
||||
getMcpSettingsFilePath: async () => '/mock/settings/path',
|
||||
dispose: async () => {},
|
||||
// Add other required public methods with no-op implementations
|
||||
restartConnection: async () => {},
|
||||
readResource: async () => ({ contents: [] }),
|
||||
callTool: async () => ({ content: [] }),
|
||||
toggleServerDisabled: async () => {},
|
||||
toggleToolAlwaysAllow: async () => {},
|
||||
isConnecting: false,
|
||||
connections: []
|
||||
} as unknown as McpHub)
|
||||
const createMockMcpHub = (): McpHub =>
|
||||
({
|
||||
getServers: () => [],
|
||||
getMcpServersPath: async () => "/mock/mcp/path",
|
||||
getMcpSettingsFilePath: async () => "/mock/settings/path",
|
||||
dispose: async () => {},
|
||||
// Add other required public methods with no-op implementations
|
||||
restartConnection: async () => {},
|
||||
readResource: async () => ({ contents: [] }),
|
||||
callTool: async () => ({ content: [] }),
|
||||
toggleServerDisabled: async () => {},
|
||||
toggleToolAlwaysAllow: async () => {},
|
||||
isConnecting: false,
|
||||
connections: [],
|
||||
}) as unknown as McpHub
|
||||
|
||||
describe('SYSTEM_PROMPT', () => {
|
||||
let mockMcpHub: McpHub
|
||||
describe("SYSTEM_PROMPT", () => {
|
||||
let mockMcpHub: McpHub
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up any McpHub instances
|
||||
if (mockMcpHub) {
|
||||
await mockMcpHub.dispose()
|
||||
}
|
||||
})
|
||||
afterEach(async () => {
|
||||
// Clean up any McpHub instances
|
||||
if (mockMcpHub) {
|
||||
await mockMcpHub.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('should maintain consistent system prompt', async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined // browserViewportSize
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
it("should maintain consistent system prompt", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
undefined, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
)
|
||||
|
||||
it('should include browser actions when supportsComputerUse is true', async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'1280x800'
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include MCP server info when mcpHub is provided', async () => {
|
||||
mockMcpHub = createMockMcpHub()
|
||||
it("should include browser actions when supportsComputerUse is true", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", true, undefined, undefined, "1280x800")
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
mockMcpHub
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should explicitly handle undefined mcpHub', async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined, // explicitly undefined mcpHub
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
it("should include MCP server info when mcpHub is provided", async () => {
|
||||
mockMcpHub = createMockMcpHub()
|
||||
|
||||
it('should handle different browser viewport sizes', async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
'900x600' // different viewport size
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, mockMcpHub)
|
||||
|
||||
it('should include diff strategy tool description', async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
'/test/path',
|
||||
false,
|
||||
undefined,
|
||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
it("should explicitly handle undefined mcpHub", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
"/test/path",
|
||||
false,
|
||||
undefined, // explicitly undefined mcpHub
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should handle different browser viewport sizes", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
"/test/path",
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
"900x600", // different viewport size
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include diff strategy tool description", async () => {
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
"/test/path",
|
||||
false,
|
||||
undefined,
|
||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||
undefined,
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addCustomInstructions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
describe("addCustomInstructions", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should prioritize mode-specific rules for code mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should generate correct prompt for architect mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "architect")
|
||||
|
||||
it('should prioritize mode-specific rules for ask mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
askMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should prioritize mode-specific rules for architect mode', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
architectMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should generate correct prompt for ask mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "ask")
|
||||
|
||||
it('should fall back to generic rules when mode-specific rules not found', async () => {
|
||||
// Mock readFile to return ENOENT for mode-specific file
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith('.clinerules-code')) {
|
||||
const error = new Error('ENOENT') as NodeJS.ErrnoException
|
||||
error.code = 'ENOENT'
|
||||
throw error
|
||||
}
|
||||
if (path.endsWith('.clinerules')) {
|
||||
return '# Test Rules\n1. First rule\n2. Second rule'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
jest.spyOn(fs, 'readFile').mockImplementation(mockReadFile)
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should prioritize mode-specific rules for code mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include preferred language when provided', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ preferredLanguage: 'Spanish' },
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should prioritize mode-specific rules for ask mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", modes[2].slug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should include custom instructions when provided', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: 'Custom test instructions' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should prioritize mode-specific rules for architect mode", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path", modes[1].slug)
|
||||
|
||||
it('should combine all custom instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: 'Custom test instructions',
|
||||
preferredLanguage: 'French'
|
||||
},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle undefined mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{},
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should prioritize mode-specific rules for test engineer mode", async () => {
|
||||
// Mock readFile to include test engineer rules
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith(".clinerules-test")) {
|
||||
return "# Test Engineer Rules\n1. Always write tests first\n2. Get approval before modifying non-test code"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
it('should trim mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: ' Custom mode instructions ' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
const instructions = await addCustomInstructions({}, "/test/path", "test")
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle empty mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: '' },
|
||||
'/test/path'
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should prioritize mode-specific rules for code reviewer mode", async () => {
|
||||
// Mock readFile to include code reviewer rules
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (path.endsWith(".clinerules-review")) {
|
||||
return "# Code Reviewer Rules\n1. Provide specific examples in feedback\n2. Focus on maintainability and best practices"
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
it('should combine global and mode-specific instructions', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: 'Global instructions',
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Mode-specific instructions' }
|
||||
}
|
||||
},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
const instructions = await addCustomInstructions({}, "/test/path", "review")
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should prioritize mode-specific instructions after global ones', async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: 'First instruction',
|
||||
customPrompts: {
|
||||
code: { customInstructions: 'Second instruction' }
|
||||
}
|
||||
},
|
||||
'/test/path',
|
||||
codeMode
|
||||
)
|
||||
|
||||
const instructionParts = instructions.split('\n\n')
|
||||
const globalIndex = instructionParts.findIndex(part => part.includes('First instruction'))
|
||||
const modeSpecificIndex = instructionParts.findIndex(part => part.includes('Second instruction'))
|
||||
|
||||
expect(globalIndex).toBeLessThan(modeSpecificIndex)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
it("should generate correct prompt for test engineer mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "test")
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
// Verify test engineer role requirements
|
||||
expect(prompt).toContain("must ask the user to confirm before making ANY changes to non-test code")
|
||||
expect(prompt).toContain("ask the user to confirm your test plan")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct prompt for code reviewer mode", async () => {
|
||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "review")
|
||||
|
||||
// Verify code reviewer role constraints
|
||||
expect(prompt).toContain("providing detailed, actionable feedback")
|
||||
expect(prompt).toContain("maintain a read-only approach")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should fall back to generic rules when mode-specific rules not found", async () => {
|
||||
// Mock readFile to return ENOENT for mode-specific file
|
||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
||||
if (
|
||||
path.endsWith(".clinerules-code") ||
|
||||
path.endsWith(".clinerules-test") ||
|
||||
path.endsWith(".clinerules-review")
|
||||
) {
|
||||
const error = new Error("ENOENT") as NodeJS.ErrnoException
|
||||
error.code = "ENOENT"
|
||||
throw error
|
||||
}
|
||||
if (path.endsWith(".clinerules")) {
|
||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
||||
|
||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include preferred language when provided", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ preferredLanguage: "Spanish" },
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include custom instructions when provided", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: "Custom test instructions" },
|
||||
"/test/path",
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should combine all custom instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "Custom test instructions",
|
||||
preferredLanguage: "French",
|
||||
},
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should handle undefined mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions({}, "/test/path")
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should trim mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions: " Custom mode instructions " },
|
||||
"/test/path",
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should handle empty mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions({ customInstructions: "" }, "/test/path")
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should combine global and mode-specific instructions", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "Global instructions",
|
||||
customPrompts: {
|
||||
code: { customInstructions: "Mode-specific instructions" },
|
||||
},
|
||||
},
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific instructions after global ones", async () => {
|
||||
const instructions = await addCustomInstructions(
|
||||
{
|
||||
customInstructions: "First instruction",
|
||||
customPrompts: {
|
||||
code: { customInstructions: "Second instruction" },
|
||||
},
|
||||
},
|
||||
"/test/path",
|
||||
defaultModeSlug,
|
||||
)
|
||||
|
||||
const instructionParts = instructions.split("\n\n")
|
||||
const globalIndex = instructionParts.findIndex((part) => part.includes("First instruction"))
|
||||
const modeSpecificIndex = instructionParts.findIndex((part) => part.includes("Second instruction"))
|
||||
|
||||
expect(globalIndex).toBeLessThan(modeSpecificIndex)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { architectMode, defaultPrompts, PromptComponent } from "../../shared/modes"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
getCapabilitiesSection
|
||||
} from "./sections"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export const mode = architectMode
|
||||
|
||||
export const ARCHITECT_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
customPrompt?: PromptComponent,
|
||||
) => `${customPrompt?.roleDefinition || defaultPrompts[architectMode].roleDefinition}
|
||||
|
||||
${getSharedToolUseSection()}
|
||||
|
||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
||||
|
||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
||||
|
||||
${getSystemInfoSection(cwd)}
|
||||
|
||||
${getObjectiveSection()}`
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Mode, askMode, defaultPrompts, PromptComponent } from "../../shared/modes"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
getCapabilitiesSection
|
||||
} from "./sections"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export const mode = askMode
|
||||
|
||||
export const ASK_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
customPrompt?: PromptComponent,
|
||||
) => `${customPrompt?.roleDefinition || defaultPrompts[askMode].roleDefinition}
|
||||
|
||||
${getSharedToolUseSection()}
|
||||
|
||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
||||
|
||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
||||
|
||||
${getSystemInfoSection(cwd)}
|
||||
|
||||
${getObjectiveSection()}`
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Mode, codeMode, defaultPrompts, PromptComponent } from "../../shared/modes"
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
getCapabilitiesSection
|
||||
} from "./sections"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export const mode: Mode = codeMode
|
||||
|
||||
export const CODE_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
customPrompt?: PromptComponent,
|
||||
) => `${customPrompt?.roleDefinition || defaultPrompts[codeMode].roleDefinition}
|
||||
|
||||
${getSharedToolUseSection()}
|
||||
|
||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
||||
|
||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
||||
|
||||
${getSystemInfoSection(cwd)}
|
||||
|
||||
${getObjectiveSection()}`
|
||||
@@ -1,5 +0,0 @@
|
||||
export const codeMode = 'code' as const;
|
||||
export const architectMode = 'architect' as const;
|
||||
export const askMode = 'ask' as const;
|
||||
|
||||
export type Mode = typeof codeMode | typeof architectMode | typeof askMode;
|
||||
@@ -2,27 +2,31 @@ import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
|
||||
export function getCapabilitiesSection(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
): string {
|
||||
return `====
|
||||
return `====
|
||||
|
||||
CAPABILITIES
|
||||
|
||||
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search${
|
||||
supportsComputerUse ? ", use the browser" : ""
|
||||
}, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
|
||||
supportsComputerUse ? ", use the browser" : ""
|
||||
}, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
|
||||
- When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('${cwd}') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file ${diffStrategy ? "or apply_diff " : ""}tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.${
|
||||
supportsComputerUse
|
||||
? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser."
|
||||
: ""
|
||||
}${mcpHub ? `
|
||||
supportsComputerUse
|
||||
? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser."
|
||||
: ""
|
||||
}${
|
||||
mcpHub
|
||||
? `
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
` : ''}`
|
||||
}
|
||||
`
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
export async function loadRuleFiles(cwd: string): Promise<string> {
|
||||
const ruleFiles = ['.clinerules', '.cursorrules', '.windsurfrules']
|
||||
let combinedRules = ''
|
||||
const ruleFiles = [".clinerules", ".cursorrules", ".windsurfrules"]
|
||||
let combinedRules = ""
|
||||
|
||||
for (const file of ruleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of ruleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, file), "utf-8")
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combinedRules
|
||||
return combinedRules
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd)
|
||||
const allInstructions = []
|
||||
export async function addCustomInstructions(
|
||||
customInstructions: string,
|
||||
cwd: string,
|
||||
preferredLanguage?: string,
|
||||
): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd)
|
||||
const allInstructions = []
|
||||
|
||||
if (preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
||||
}
|
||||
|
||||
if (customInstructions.trim()) {
|
||||
allInstructions.push(customInstructions.trim())
|
||||
}
|
||||
if (preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
||||
}
|
||||
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
}
|
||||
if (customInstructions.trim()) {
|
||||
allInstructions.push(customInstructions.trim())
|
||||
}
|
||||
|
||||
const joinedInstructions = allInstructions.join('\n\n')
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
}
|
||||
|
||||
return joinedInstructions ? `
|
||||
const joinedInstructions = allInstructions.join("\n\n")
|
||||
|
||||
return joinedInstructions
|
||||
? `
|
||||
====
|
||||
|
||||
USER'S CUSTOM INSTRUCTIONS
|
||||
@@ -48,5 +53,5 @@ USER'S CUSTOM INSTRUCTIONS
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
${joinedInstructions}`
|
||||
: ""
|
||||
}
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export { getRulesSection } from './rules'
|
||||
export { getSystemInfoSection } from './system-info'
|
||||
export { getObjectiveSection } from './objective'
|
||||
export { addCustomInstructions } from './custom-instructions'
|
||||
export { getSharedToolUseSection } from './tool-use'
|
||||
export { getMcpServersSection } from './mcp-servers'
|
||||
export { getToolUseGuidelinesSection } from './tool-use-guidelines'
|
||||
export { getCapabilitiesSection } from './capabilities'
|
||||
export { getRulesSection } from "./rules"
|
||||
export { getSystemInfoSection } from "./system-info"
|
||||
export { getObjectiveSection } from "./objective"
|
||||
export { addCustomInstructions } from "./custom-instructions"
|
||||
export { getSharedToolUseSection } from "./tool-use"
|
||||
export { getMcpServersSection } from "./mcp-servers"
|
||||
export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
|
||||
export { getCapabilitiesSection } from "./capabilities"
|
||||
|
||||
@@ -2,47 +2,48 @@ import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
|
||||
export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffStrategy): Promise<string> {
|
||||
if (!mcpHub) {
|
||||
return '';
|
||||
}
|
||||
if (!mcpHub) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const connectedServers = mcpHub.getServers().length > 0
|
||||
? `${mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => {
|
||||
const tools = server.tools
|
||||
?.map((tool) => {
|
||||
const schemaStr = tool.inputSchema
|
||||
? ` Input Schema:
|
||||
const connectedServers =
|
||||
mcpHub.getServers().length > 0
|
||||
? `${mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => {
|
||||
const tools = server.tools
|
||||
?.map((tool) => {
|
||||
const schemaStr = tool.inputSchema
|
||||
? ` Input Schema:
|
||||
${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
|
||||
: ""
|
||||
: ""
|
||||
|
||||
return `- ${tool.name}: ${tool.description}\n${schemaStr}`
|
||||
})
|
||||
.join("\n\n")
|
||||
return `- ${tool.name}: ${tool.description}\n${schemaStr}`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
const templates = server.resourceTemplates
|
||||
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
|
||||
.join("\n")
|
||||
const templates = server.resourceTemplates
|
||||
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
|
||||
.join("\n")
|
||||
|
||||
const resources = server.resources
|
||||
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
|
||||
.join("\n")
|
||||
const resources = server.resources
|
||||
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
|
||||
.join("\n")
|
||||
|
||||
const config = JSON.parse(server.config)
|
||||
const config = JSON.parse(server.config)
|
||||
|
||||
return (
|
||||
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
|
||||
(tools ? `\n\n### Available Tools\n${tools}` : "") +
|
||||
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "")
|
||||
)
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)";
|
||||
return (
|
||||
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
|
||||
(tools ? `\n\n### Available Tools\n${tools}` : "") +
|
||||
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "")
|
||||
)
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
|
||||
return `MCP SERVERS
|
||||
return `MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
|
||||
@@ -397,11 +398,11 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
|
||||
mcpHub
|
||||
.getServers()
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
@@ -410,4 +411,4 @@ However some MCP servers may be running from installed packages rather than a lo
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getObjectiveSection(): string {
|
||||
return `====
|
||||
return `====
|
||||
|
||||
OBJECTIVE
|
||||
|
||||
@@ -10,4 +10,4 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within <thinking></thinking> tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
|
||||
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
|
||||
export function getRulesSection(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy
|
||||
): string {
|
||||
return `====
|
||||
export function getRulesSection(cwd: string, supportsComputerUse: boolean, diffStrategy?: DiffStrategy): string {
|
||||
return `====
|
||||
|
||||
RULES
|
||||
|
||||
@@ -23,10 +19,10 @@ ${diffStrategy ? "- You should use apply_diff instead of write_to_file when maki
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
|
||||
supportsComputerUse
|
||||
? '\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.'
|
||||
: ""
|
||||
}
|
||||
supportsComputerUse
|
||||
? '\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.'
|
||||
: ""
|
||||
}
|
||||
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've updated the CSS" but instead something like "I've updated the CSS". It is important you be clear and technical in your messages.
|
||||
- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
|
||||
@@ -35,8 +31,8 @@ ${diffStrategy ? "- You should use apply_diff instead of write_to_file when maki
|
||||
- When using the write_to_file tool, ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project.
|
||||
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
|
||||
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.${
|
||||
supportsComputerUse
|
||||
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
supportsComputerUse
|
||||
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "os"
|
||||
import osName from "os-name"
|
||||
|
||||
export function getSystemInfoSection(cwd: string): string {
|
||||
return `====
|
||||
return `====
|
||||
|
||||
SYSTEM INFORMATION
|
||||
|
||||
@@ -13,4 +13,4 @@ Home Directory: ${os.homedir().toPosix()}
|
||||
Current Working Directory: ${cwd.toPosix()}
|
||||
|
||||
When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getToolUseGuidelinesSection(): string {
|
||||
return `# Tool Use Guidelines
|
||||
return `# Tool Use Guidelines
|
||||
|
||||
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
|
||||
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
|
||||
@@ -19,4 +19,4 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
|
||||
4. Ensure that each action builds correctly on the previous ones.
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getSharedToolUseSection(): string {
|
||||
return `====
|
||||
return `====
|
||||
|
||||
TOOL USE
|
||||
|
||||
@@ -22,4 +22,4 @@ For example:
|
||||
</read_file>
|
||||
|
||||
Always adhere to this format for the tool use to ensure proper parsing and execution.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,86 @@
|
||||
import { Mode, modes, CustomPrompts, PromptComponent, getRoleDefinition, defaultModeSlug } from "../../shared/modes"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { CODE_PROMPT } from "./code"
|
||||
import { ARCHITECT_PROMPT } from "./architect"
|
||||
import { ASK_PROMPT } from "./ask"
|
||||
import { Mode, codeMode, architectMode, askMode } from "./modes"
|
||||
import { CustomPrompts } from "../../shared/modes"
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getToolDescriptionsForMode } from "./tools"
|
||||
import {
|
||||
getRulesSection,
|
||||
getSystemInfoSection,
|
||||
getObjectiveSection,
|
||||
getSharedToolUseSection,
|
||||
getMcpServersSection,
|
||||
getToolUseGuidelinesSection,
|
||||
getCapabilitiesSection,
|
||||
} from "./sections"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
async function loadRuleFiles(cwd: string, mode: Mode): Promise<string> {
|
||||
let combinedRules = ''
|
||||
let combinedRules = ""
|
||||
|
||||
// First try mode-specific rules
|
||||
const modeSpecificFile = `.clinerules-${mode}`
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, modeSpecificFile), 'utf-8')
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${modeSpecificFile}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// First try mode-specific rules
|
||||
const modeSpecificFile = `.clinerules-${mode}`
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, modeSpecificFile), "utf-8")
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${modeSpecificFile}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Then try generic rules files
|
||||
const genericRuleFiles = ['.clinerules']
|
||||
for (const file of genericRuleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then try generic rules files
|
||||
const genericRuleFiles = [".clinerules"]
|
||||
for (const file of genericRuleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(cwd, file), "utf-8")
|
||||
if (content.trim()) {
|
||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently skip if file doesn't exist
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combinedRules
|
||||
return combinedRules
|
||||
}
|
||||
|
||||
interface State {
|
||||
customInstructions?: string;
|
||||
customPrompts?: CustomPrompts;
|
||||
preferredLanguage?: string;
|
||||
customInstructions?: string
|
||||
customPrompts?: CustomPrompts
|
||||
preferredLanguage?: string
|
||||
}
|
||||
|
||||
export async function addCustomInstructions(
|
||||
state: State,
|
||||
cwd: string,
|
||||
mode: Mode = codeMode
|
||||
): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd, mode)
|
||||
const allInstructions = []
|
||||
export async function addCustomInstructions(state: State, cwd: string, mode: Mode = defaultModeSlug): Promise<string> {
|
||||
const ruleFileContent = await loadRuleFiles(cwd, mode)
|
||||
const allInstructions = []
|
||||
|
||||
if (state.preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
||||
}
|
||||
if (state.preferredLanguage) {
|
||||
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
||||
}
|
||||
|
||||
if (state.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customInstructions.trim())
|
||||
}
|
||||
if (state.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customInstructions.trim())
|
||||
}
|
||||
|
||||
if (state.customPrompts?.[mode]?.customInstructions?.trim()) {
|
||||
allInstructions.push(state.customPrompts[mode].customInstructions.trim())
|
||||
}
|
||||
const customPrompt = state.customPrompts?.[mode]
|
||||
if (typeof customPrompt === "object" && customPrompt?.customInstructions?.trim()) {
|
||||
allInstructions.push(customPrompt.customInstructions.trim())
|
||||
}
|
||||
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
}
|
||||
if (ruleFileContent && ruleFileContent.trim()) {
|
||||
allInstructions.push(ruleFileContent.trim())
|
||||
}
|
||||
|
||||
const joinedInstructions = allInstructions.join('\n\n')
|
||||
const joinedInstructions = allInstructions.join("\n\n")
|
||||
|
||||
return joinedInstructions ? `
|
||||
return joinedInstructions
|
||||
? `
|
||||
====
|
||||
|
||||
USER'S CUSTOM INSTRUCTIONS
|
||||
@@ -84,26 +88,66 @@ USER'S CUSTOM INSTRUCTIONS
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||
|
||||
${joinedInstructions}`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
|
||||
async function generatePrompt(
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mode: Mode,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
promptComponent?: PromptComponent,
|
||||
): Promise<string> {
|
||||
const basePrompt = `${promptComponent?.roleDefinition || getRoleDefinition(mode)}
|
||||
|
||||
${getSharedToolUseSection()}
|
||||
|
||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
||||
|
||||
${getToolUseGuidelinesSection()}
|
||||
|
||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
||||
|
||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||
|
||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
||||
|
||||
${getSystemInfoSection(cwd)}
|
||||
|
||||
${getObjectiveSection()}`
|
||||
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mode: Mode = codeMode,
|
||||
customPrompts?: CustomPrompts,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mode: Mode = defaultModeSlug,
|
||||
customPrompts?: CustomPrompts,
|
||||
) => {
|
||||
switch (mode) {
|
||||
case architectMode:
|
||||
return ARCHITECT_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.architect)
|
||||
case askMode:
|
||||
return ASK_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.ask)
|
||||
default:
|
||||
return CODE_PROMPT(cwd, supportsComputerUse, mcpHub, diffStrategy, browserViewportSize, customPrompts?.code)
|
||||
}
|
||||
}
|
||||
const getPromptComponent = (value: unknown) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return value as PromptComponent
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export { codeMode, architectMode, askMode }
|
||||
// Use default mode if not found
|
||||
const currentMode = modes.find((m) => m.slug === mode) || modes[0]
|
||||
const promptComponent = getPromptComponent(customPrompts?.[currentMode.slug])
|
||||
|
||||
return generatePrompt(
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
currentMode.slug,
|
||||
mcpHub,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
promptComponent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export function getAccessMcpResourceDescription(): string {
|
||||
return `## access_mcp_resource
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getAccessMcpResourceDescription(args: ToolArgs): string | undefined {
|
||||
if (!args.mcpHub) {
|
||||
return undefined
|
||||
}
|
||||
return `## access_mcp_resource
|
||||
Description: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.
|
||||
Parameters:
|
||||
- server_name: (required) The name of the MCP server providing the resource
|
||||
@@ -16,4 +21,4 @@ Example: Requesting to access an MCP resource
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getAskFollowupQuestionDescription(): string {
|
||||
return `## ask_followup_question
|
||||
return `## ask_followup_question
|
||||
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
|
||||
Parameters:
|
||||
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
|
||||
@@ -12,4 +12,4 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
|
||||
<ask_followup_question>
|
||||
<question>What is the path to the frontend-config.json file?</question>
|
||||
</ask_followup_question>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function getAttemptCompletionDescription(): string {
|
||||
return `## attempt_completion
|
||||
return `## attempt_completion
|
||||
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
|
||||
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.
|
||||
Parameters:
|
||||
@@ -20,4 +20,4 @@ I've updated the CSS
|
||||
</result>
|
||||
<command>open index.html</command>
|
||||
</attempt_completion>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
export function getBrowserActionDescription(cwd: string, browserViewportSize: string = "900x600"): string {
|
||||
return `## browser_action
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getBrowserActionDescription(args: ToolArgs): string | undefined {
|
||||
if (!args.supportsComputerUse) {
|
||||
return undefined
|
||||
}
|
||||
return `## browser_action
|
||||
Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
|
||||
- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.
|
||||
- While the browser is active, only the \`browser_action\` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.
|
||||
- The browser window has a resolution of **${browserViewportSize}** pixels. When performing any click actions, ensure the coordinates are within this resolution range.
|
||||
- The browser window has a resolution of **${args.browserViewportSize}** pixels. When performing any click actions, ensure the coordinates are within this resolution range.
|
||||
- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.
|
||||
Parameters:
|
||||
- action: (required) The action to perform. The available actions are:
|
||||
@@ -21,7 +26,7 @@ Parameters:
|
||||
- Example: \`<action>close</action>\`
|
||||
- url: (optional) Use this for providing the URL for the \`launch\` action.
|
||||
* Example: <url>https://example.com</url>
|
||||
- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **${browserViewportSize}** resolution.
|
||||
- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **${args.browserViewportSize}** resolution.
|
||||
* Example: <coordinate>450,300</coordinate>
|
||||
- text: (optional) Use this for providing the text for the \`type\` action.
|
||||
* Example: <text>Hello, world!</text>
|
||||
@@ -44,4 +49,4 @@ Example: Requesting to click on the element at coordinates 450,300
|
||||
<action>click</action>
|
||||
<coordinate>450,300</coordinate>
|
||||
</browser_action>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export function getExecuteCommandDescription(cwd: string): string {
|
||||
return `## execute_command
|
||||
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd}
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
|
||||
return `## execute_command
|
||||
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${args.cwd}
|
||||
Parameters:
|
||||
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
|
||||
Usage:
|
||||
@@ -12,4 +14,4 @@ Example: Requesting to execute npm run dev
|
||||
<execute_command>
|
||||
<command>npm run dev</command>
|
||||
</execute_command>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,80 @@
|
||||
import { getExecuteCommandDescription } from './execute-command'
|
||||
import { getReadFileDescription } from './read-file'
|
||||
import { getWriteToFileDescription } from './write-to-file'
|
||||
import { getSearchFilesDescription } from './search-files'
|
||||
import { getListFilesDescription } from './list-files'
|
||||
import { getListCodeDefinitionNamesDescription } from './list-code-definition-names'
|
||||
import { getBrowserActionDescription } from './browser-action'
|
||||
import { getAskFollowupQuestionDescription } from './ask-followup-question'
|
||||
import { getAttemptCompletionDescription } from './attempt-completion'
|
||||
import { getUseMcpToolDescription } from './use-mcp-tool'
|
||||
import { getAccessMcpResourceDescription } from './access-mcp-resource'
|
||||
import { DiffStrategy } from '../../diff/DiffStrategy'
|
||||
import { McpHub } from '../../../services/mcp/McpHub'
|
||||
import { Mode, codeMode, askMode } from '../modes'
|
||||
import { CODE_ALLOWED_TOOLS, READONLY_ALLOWED_TOOLS, ToolName, ReadOnlyToolName } from '../../tool-lists'
|
||||
import { getExecuteCommandDescription } from "./execute-command"
|
||||
import { getReadFileDescription } from "./read-file"
|
||||
import { getWriteToFileDescription } from "./write-to-file"
|
||||
import { getSearchFilesDescription } from "./search-files"
|
||||
import { getListFilesDescription } from "./list-files"
|
||||
import { getListCodeDefinitionNamesDescription } from "./list-code-definition-names"
|
||||
import { getBrowserActionDescription } from "./browser-action"
|
||||
import { getAskFollowupQuestionDescription } from "./ask-followup-question"
|
||||
import { getAttemptCompletionDescription } from "./attempt-completion"
|
||||
import { getUseMcpToolDescription } from "./use-mcp-tool"
|
||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { Mode, ToolName, getModeConfig, isToolAllowedForMode } from "../../../shared/modes"
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
type AllToolNames = ToolName | ReadOnlyToolName;
|
||||
|
||||
// Helper function to safely check if a tool is allowed
|
||||
function hasAllowedTool(tools: readonly string[], tool: AllToolNames): boolean {
|
||||
return tools.includes(tool);
|
||||
// Map of tool names to their description functions
|
||||
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
|
||||
execute_command: (args) => getExecuteCommandDescription(args),
|
||||
read_file: (args) => getReadFileDescription(args),
|
||||
write_to_file: (args) => getWriteToFileDescription(args),
|
||||
search_files: (args) => getSearchFilesDescription(args),
|
||||
list_files: (args) => getListFilesDescription(args),
|
||||
list_code_definition_names: (args) => getListCodeDefinitionNamesDescription(args),
|
||||
browser_action: (args) => getBrowserActionDescription(args),
|
||||
ask_followup_question: () => getAskFollowupQuestionDescription(),
|
||||
attempt_completion: () => getAttemptCompletionDescription(),
|
||||
use_mcp_tool: (args) => getUseMcpToolDescription(args),
|
||||
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
|
||||
apply_diff: (args) =>
|
||||
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
|
||||
}
|
||||
|
||||
export function getToolDescriptionsForMode(
|
||||
mode: Mode,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mcpHub?: McpHub
|
||||
mode: Mode,
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
diffStrategy?: DiffStrategy,
|
||||
browserViewportSize?: string,
|
||||
mcpHub?: McpHub,
|
||||
): string {
|
||||
const descriptions = []
|
||||
const config = getModeConfig(mode)
|
||||
const args: ToolArgs = {
|
||||
cwd,
|
||||
supportsComputerUse,
|
||||
diffStrategy,
|
||||
browserViewportSize,
|
||||
mcpHub,
|
||||
}
|
||||
|
||||
const allowedTools = mode === codeMode ? CODE_ALLOWED_TOOLS : READONLY_ALLOWED_TOOLS;
|
||||
// Map tool descriptions in the exact order specified in the mode's tools array
|
||||
const descriptions = config.tools.map(([toolName, toolOptions]) => {
|
||||
const descriptionFn = toolDescriptionMap[toolName]
|
||||
if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Core tools based on mode
|
||||
if (hasAllowedTool(allowedTools, 'execute_command')) {
|
||||
descriptions.push(getExecuteCommandDescription(cwd));
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'read_file')) {
|
||||
descriptions.push(getReadFileDescription(cwd));
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'write_to_file')) {
|
||||
descriptions.push(getWriteToFileDescription(cwd));
|
||||
}
|
||||
return descriptionFn({
|
||||
...args,
|
||||
toolOptions,
|
||||
})
|
||||
})
|
||||
|
||||
// Optional diff strategy
|
||||
if (diffStrategy && hasAllowedTool(allowedTools, 'apply_diff')) {
|
||||
descriptions.push(diffStrategy.getToolDescription(cwd));
|
||||
}
|
||||
|
||||
// File operation tools
|
||||
if (hasAllowedTool(allowedTools, 'search_files')) {
|
||||
descriptions.push(getSearchFilesDescription(cwd));
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'list_files')) {
|
||||
descriptions.push(getListFilesDescription(cwd));
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'list_code_definition_names')) {
|
||||
descriptions.push(getListCodeDefinitionNamesDescription(cwd));
|
||||
}
|
||||
|
||||
// Browser actions
|
||||
if (supportsComputerUse && hasAllowedTool(allowedTools, 'browser_action')) {
|
||||
descriptions.push(getBrowserActionDescription(cwd, browserViewportSize));
|
||||
}
|
||||
|
||||
// Common tools at the end
|
||||
if (hasAllowedTool(allowedTools, 'ask_followup_question')) {
|
||||
descriptions.push(getAskFollowupQuestionDescription());
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'attempt_completion')) {
|
||||
descriptions.push(getAttemptCompletionDescription());
|
||||
}
|
||||
|
||||
// MCP tools if available
|
||||
if (mcpHub) {
|
||||
if (hasAllowedTool(allowedTools, 'use_mcp_tool')) {
|
||||
descriptions.push(getUseMcpToolDescription());
|
||||
}
|
||||
if (hasAllowedTool(allowedTools, 'access_mcp_resource')) {
|
||||
descriptions.push(getAccessMcpResourceDescription());
|
||||
}
|
||||
}
|
||||
|
||||
return `# Tools\n\n${descriptions.filter(Boolean).join('\n\n')}`
|
||||
return `# Tools\n\n${descriptions.filter(Boolean).join("\n\n")}`
|
||||
}
|
||||
|
||||
// Export individual description functions for backward compatibility
|
||||
export {
|
||||
getExecuteCommandDescription,
|
||||
getReadFileDescription,
|
||||
getWriteToFileDescription,
|
||||
getSearchFilesDescription,
|
||||
getListFilesDescription,
|
||||
getListCodeDefinitionNamesDescription,
|
||||
getBrowserActionDescription,
|
||||
getAskFollowupQuestionDescription,
|
||||
getAttemptCompletionDescription,
|
||||
getUseMcpToolDescription,
|
||||
getAccessMcpResourceDescription
|
||||
}
|
||||
getExecuteCommandDescription,
|
||||
getReadFileDescription,
|
||||
getWriteToFileDescription,
|
||||
getSearchFilesDescription,
|
||||
getListFilesDescription,
|
||||
getListCodeDefinitionNamesDescription,
|
||||
getBrowserActionDescription,
|
||||
getAskFollowupQuestionDescription,
|
||||
getAttemptCompletionDescription,
|
||||
getUseMcpToolDescription,
|
||||
getAccessMcpResourceDescription,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function getListCodeDefinitionNamesDescription(cwd: string): string {
|
||||
return `## list_code_definition_names
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getListCodeDefinitionNamesDescription(args: ToolArgs): string {
|
||||
return `## list_code_definition_names
|
||||
Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory (relative to the current working directory ${cwd.toPosix()}) to list top level source code definitions for.
|
||||
- path: (required) The path of the directory (relative to the current working directory ${args.cwd}) to list top level source code definitions for.
|
||||
Usage:
|
||||
<list_code_definition_names>
|
||||
<path>Directory path here</path>
|
||||
@@ -12,4 +14,4 @@ Example: Requesting to list all top level source code definitions in the current
|
||||
<list_code_definition_names>
|
||||
<path>.</path>
|
||||
</list_code_definition_names>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function getListFilesDescription(cwd: string): string {
|
||||
return `## list_files
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getListFilesDescription(args: ToolArgs): string {
|
||||
return `## list_files
|
||||
Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to list contents for (relative to the current working directory ${cwd.toPosix()})
|
||||
- path: (required) The path of the directory to list contents for (relative to the current working directory ${args.cwd})
|
||||
- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
|
||||
Usage:
|
||||
<list_files>
|
||||
@@ -15,4 +17,4 @@ Example: Requesting to list all files in the current directory
|
||||
<path>.</path>
|
||||
<recursive>false</recursive>
|
||||
</list_files>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function getReadFileDescription(cwd: string): string {
|
||||
return `## read_file
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getReadFileDescription(args: ToolArgs): string {
|
||||
return `## read_file
|
||||
Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to read (relative to the current working directory ${cwd})
|
||||
- path: (required) The path of the file to read (relative to the current working directory ${args.cwd})
|
||||
Usage:
|
||||
<read_file>
|
||||
<path>File path here</path>
|
||||
@@ -12,4 +14,4 @@ Example: Requesting to read frontend-config.json
|
||||
<read_file>
|
||||
<path>frontend-config.json</path>
|
||||
</read_file>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function getSearchFilesDescription(cwd: string): string {
|
||||
return `## search_files
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getSearchFilesDescription(args: ToolArgs): string {
|
||||
return `## search_files
|
||||
Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
|
||||
Parameters:
|
||||
- path: (required) The path of the directory to search in (relative to the current working directory ${cwd.toPosix()}). This directory will be recursively searched.
|
||||
- path: (required) The path of the directory to search in (relative to the current working directory ${args.cwd}). This directory will be recursively searched.
|
||||
- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
|
||||
- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
|
||||
Usage:
|
||||
|
||||
11
src/core/prompts/tools/types.ts
Normal file
11
src/core/prompts/tools/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
|
||||
export type ToolArgs = {
|
||||
cwd: string
|
||||
supportsComputerUse: boolean
|
||||
diffStrategy?: DiffStrategy
|
||||
browserViewportSize?: string
|
||||
mcpHub?: McpHub
|
||||
toolOptions?: any
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
export function getUseMcpToolDescription(): string {
|
||||
return `## use_mcp_tool
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getUseMcpToolDescription(args: ToolArgs): string | undefined {
|
||||
if (!args.mcpHub) {
|
||||
return undefined
|
||||
}
|
||||
return `## use_mcp_tool
|
||||
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
|
||||
Parameters:
|
||||
- server_name: (required) The name of the MCP server providing the tool
|
||||
@@ -29,4 +34,4 @@ Example: Requesting to use an MCP tool
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export function getWriteToFileDescription(cwd: string): string {
|
||||
return `## write_to_file
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getWriteToFileDescription(args: ToolArgs): string {
|
||||
return `## write_to_file
|
||||
Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to write to (relative to the current working directory ${cwd.toPosix()})
|
||||
- path: (required) The path of the file to write to (relative to the current working directory ${args.cwd})
|
||||
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file.
|
||||
- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing.
|
||||
Usage:
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { Mode } from '../../shared/modes';
|
||||
import { Mode } from "../../shared/modes"
|
||||
|
||||
export type { Mode };
|
||||
export type { Mode }
|
||||
|
||||
export type ToolName =
|
||||
| 'execute_command'
|
||||
| 'read_file'
|
||||
| 'write_to_file'
|
||||
| 'apply_diff'
|
||||
| 'search_files'
|
||||
| 'list_files'
|
||||
| 'list_code_definition_names'
|
||||
| 'browser_action'
|
||||
| 'use_mcp_tool'
|
||||
| 'access_mcp_resource'
|
||||
| 'ask_followup_question'
|
||||
| 'attempt_completion';
|
||||
| "execute_command"
|
||||
| "read_file"
|
||||
| "write_to_file"
|
||||
| "apply_diff"
|
||||
| "search_files"
|
||||
| "list_files"
|
||||
| "list_code_definition_names"
|
||||
| "browser_action"
|
||||
| "use_mcp_tool"
|
||||
| "access_mcp_resource"
|
||||
| "ask_followup_question"
|
||||
| "attempt_completion"
|
||||
|
||||
export const CODE_TOOLS: ToolName[] = [
|
||||
'execute_command',
|
||||
'read_file',
|
||||
'write_to_file',
|
||||
'apply_diff',
|
||||
'search_files',
|
||||
'list_files',
|
||||
'list_code_definition_names',
|
||||
'browser_action',
|
||||
'use_mcp_tool',
|
||||
'access_mcp_resource',
|
||||
'ask_followup_question',
|
||||
'attempt_completion'
|
||||
];
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
|
||||
export const ARCHITECT_TOOLS: ToolName[] = [
|
||||
'read_file',
|
||||
'search_files',
|
||||
'list_files',
|
||||
'list_code_definition_names',
|
||||
'ask_followup_question',
|
||||
'attempt_completion'
|
||||
];
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
|
||||
export const ASK_TOOLS: ToolName[] = [
|
||||
'read_file',
|
||||
'search_files',
|
||||
'list_files',
|
||||
'browser_action',
|
||||
'use_mcp_tool',
|
||||
'access_mcp_resource',
|
||||
'ask_followup_question',
|
||||
'attempt_completion'
|
||||
];
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
]
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
// Shared tools for architect and ask modes - read-only operations plus MCP and browser tools
|
||||
export const READONLY_ALLOWED_TOOLS = [
|
||||
'read_file',
|
||||
'search_files',
|
||||
'list_files',
|
||||
'list_code_definition_names',
|
||||
'browser_action',
|
||||
'use_mcp_tool',
|
||||
'access_mcp_resource',
|
||||
'ask_followup_question',
|
||||
'attempt_completion'
|
||||
] as const;
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
] as const
|
||||
|
||||
// Code mode has access to all tools
|
||||
export const CODE_ALLOWED_TOOLS = [
|
||||
'execute_command',
|
||||
'read_file',
|
||||
'write_to_file',
|
||||
'apply_diff',
|
||||
'search_files',
|
||||
'list_files',
|
||||
'list_code_definition_names',
|
||||
'browser_action',
|
||||
'use_mcp_tool',
|
||||
'access_mcp_resource',
|
||||
'ask_followup_question',
|
||||
'attempt_completion'
|
||||
] as const;
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
] as const
|
||||
|
||||
// Tool name types for type safety
|
||||
export type ReadOnlyToolName = typeof READONLY_ALLOWED_TOOLS[number];
|
||||
export type ToolName = typeof CODE_ALLOWED_TOOLS[number];
|
||||
export type ReadOnlyToolName = (typeof READONLY_ALLOWED_TOOLS)[number]
|
||||
export type ToolName = (typeof CODE_ALLOWED_TOOLS)[number]
|
||||
|
||||
@@ -17,7 +17,7 @@ import { findLast } from "../../shared/array"
|
||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { WebviewMessage, PromptMode } from "../../shared/WebviewMessage"
|
||||
import { defaultPrompts } from "../../shared/modes"
|
||||
import { defaultModeSlug, defaultPrompts } from "../../shared/modes"
|
||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { Cline } from "../Cline"
|
||||
@@ -29,8 +29,7 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { enhancePrompt } from "../../utils/enhance-prompt"
|
||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||
import { ConfigManager } from "../config/ConfigManager"
|
||||
import { Mode } from "../prompts/types"
|
||||
import { codeMode, CustomPrompts } from "../../shared/modes"
|
||||
import { Mode, modes, CustomPrompts, PromptComponent, enhance } from "../../shared/modes"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -99,7 +98,7 @@ type GlobalStateKey =
|
||||
| "modeApiConfigs"
|
||||
| "customPrompts"
|
||||
| "enhancementApiConfigId"
|
||||
| "experimentalDiffStrategy"
|
||||
| "experimentalDiffStrategy"
|
||||
| "autoApprovalEnabled"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
@@ -255,13 +254,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
experimentalDiffStrategy,
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
const modePrompt = customPrompts?.[mode]
|
||||
const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n")
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
@@ -272,7 +270,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
task,
|
||||
images,
|
||||
undefined,
|
||||
experimentalDiffStrategy
|
||||
experimentalDiffStrategy,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -285,13 +283,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
experimentalDiffStrategy,
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
const modePrompt = customPrompts?.[mode]
|
||||
const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined
|
||||
const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n")
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
@@ -302,7 +299,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
experimentalDiffStrategy
|
||||
experimentalDiffStrategy,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -402,7 +399,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
async (message: WebviewMessage) => {
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch":
|
||||
|
||||
this.postStateToWebview()
|
||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||
getTheme().then((theme) =>
|
||||
@@ -449,53 +445,53 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
this.configManager.ListConfig().then(async (listApiConfig) => {
|
||||
|
||||
if (!listApiConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
if (listApiConfig.length === 1) {
|
||||
// check if first time init then sync with exist config
|
||||
if (!checkExistKey(listApiConfig[0])) {
|
||||
const {
|
||||
apiConfiguration,
|
||||
} = await this.getState()
|
||||
await this.configManager.SaveConfig(listApiConfig[0].name ?? "default", apiConfiguration)
|
||||
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
|
||||
this.configManager
|
||||
.ListConfig()
|
||||
.then(async (listApiConfig) => {
|
||||
if (!listApiConfig) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let currentConfigName = await this.getGlobalState("currentApiConfigName") as string
|
||||
|
||||
if (currentConfigName) {
|
||||
if (!await this.configManager.HasConfig(currentConfigName)) {
|
||||
// current config name not valid, get first config in list
|
||||
await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
|
||||
if (listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(listApiConfig?.[0]?.name);
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
if (listApiConfig.length === 1) {
|
||||
// check if first time init then sync with exist config
|
||||
if (!checkExistKey(listApiConfig[0])) {
|
||||
const { apiConfiguration } = await this.getState()
|
||||
await this.configManager.SaveConfig(
|
||||
listApiConfig[0].name ?? "default",
|
||||
apiConfiguration,
|
||||
)
|
||||
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
if (currentConfigName) {
|
||||
if (!(await this.configManager.HasConfig(currentConfigName))) {
|
||||
// current config name not valid, get first config in list
|
||||
await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
|
||||
if (listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(
|
||||
listApiConfig?.[0]?.name,
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||
]
|
||||
)
|
||||
}).catch(console.error);
|
||||
await this.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
|
||||
])
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
break
|
||||
case "newTask":
|
||||
@@ -592,7 +588,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
break
|
||||
case "refreshOpenAiModels":
|
||||
if (message?.values?.baseUrl && message?.values?.apiKey) {
|
||||
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
|
||||
const openAiModels = await this.getOpenAiModels(
|
||||
message?.values?.baseUrl,
|
||||
message?.values?.apiKey,
|
||||
)
|
||||
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
||||
}
|
||||
break
|
||||
@@ -624,12 +623,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
break
|
||||
case "allowedCommands":
|
||||
await this.context.globalState.update('allowedCommands', message.commands);
|
||||
await this.context.globalState.update("allowedCommands", message.commands)
|
||||
// Also update workspace settings
|
||||
await vscode.workspace
|
||||
.getConfiguration('roo-cline')
|
||||
.update('allowedCommands', message.commands, vscode.ConfigurationTarget.Global);
|
||||
break;
|
||||
.getConfiguration("roo-cline")
|
||||
.update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global)
|
||||
break
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
@@ -650,7 +649,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.mcpHub?.toggleToolAlwaysAllow(
|
||||
message.serverName!,
|
||||
message.toolName!,
|
||||
message.alwaysAllow!
|
||||
message.alwaysAllow!,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle auto-approve for tool ${message.toolName}:`, error)
|
||||
@@ -659,10 +658,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
case "toggleMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.toggleServerDisabled(
|
||||
message.serverName!,
|
||||
message.disabled!
|
||||
)
|
||||
await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
|
||||
}
|
||||
@@ -682,7 +678,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "soundEnabled":
|
||||
const soundEnabled = message.bool ?? true
|
||||
await this.updateGlobalState("soundEnabled", soundEnabled)
|
||||
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
||||
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "soundVolume":
|
||||
@@ -728,84 +724,84 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "mode":
|
||||
const newMode = message.text as Mode
|
||||
await this.updateGlobalState("mode", newMode)
|
||||
|
||||
|
||||
// Load the saved API config for the new mode if it exists
|
||||
const savedConfigId = await this.configManager.GetModeConfigId(newMode)
|
||||
const listApiConfig = await this.configManager.ListConfig()
|
||||
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
|
||||
// If this mode has a saved config, use it
|
||||
if (savedConfigId) {
|
||||
const config = listApiConfig?.find(c => c.id === savedConfigId)
|
||||
const config = listApiConfig?.find((c) => c.id === savedConfigId)
|
||||
if (config?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(config.name)
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", config.name),
|
||||
this.updateApiConfiguration(apiConfig)
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
// If no saved config for this mode, save current config as default
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
if (currentApiConfigName) {
|
||||
const config = listApiConfig?.find(c => c.name === currentApiConfigName)
|
||||
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
|
||||
if (config?.id) {
|
||||
await this.configManager.SetModeConfig(newMode, config.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "updateEnhancedPrompt":
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const existingPrompts = (await this.getGlobalState("customPrompts")) || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
enhance: message.text
|
||||
enhance: message.text,
|
||||
}
|
||||
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
customPrompts: updatedPrompts,
|
||||
}
|
||||
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
state: stateWithPrompts,
|
||||
})
|
||||
break
|
||||
case "updatePrompt":
|
||||
if (message.promptMode && message.customPrompt !== undefined) {
|
||||
const existingPrompts = await this.getGlobalState("customPrompts") || {}
|
||||
|
||||
const existingPrompts = (await this.getGlobalState("customPrompts")) || {}
|
||||
|
||||
const updatedPrompts = {
|
||||
...existingPrompts,
|
||||
[message.promptMode]: message.customPrompt
|
||||
[message.promptMode]: message.customPrompt,
|
||||
}
|
||||
|
||||
|
||||
await this.updateGlobalState("customPrompts", updatedPrompts)
|
||||
|
||||
|
||||
// Get current state and explicitly include customPrompts
|
||||
const currentState = await this.getState()
|
||||
|
||||
|
||||
const stateWithPrompts = {
|
||||
...currentState,
|
||||
customPrompts: updatedPrompts
|
||||
customPrompts: updatedPrompts,
|
||||
}
|
||||
|
||||
|
||||
// Post state with prompts
|
||||
this.view?.webview.postMessage({
|
||||
type: "state",
|
||||
state: stateWithPrompts
|
||||
state: stateWithPrompts,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -816,60 +812,79 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
"Just this message",
|
||||
"This and all subsequent messages",
|
||||
)
|
||||
if ((answer === "Just this message" || answer === "This and all subsequent messages") &&
|
||||
this.cline && typeof message.value === 'number' && message.value) {
|
||||
const timeCutoff = message.value - 1000; // 1 second buffer before the message to delete
|
||||
const messageIndex = this.cline.clineMessages.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
|
||||
const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
|
||||
|
||||
if (
|
||||
(answer === "Just this message" || answer === "This and all subsequent messages") &&
|
||||
this.cline &&
|
||||
typeof message.value === "number" &&
|
||||
message.value
|
||||
) {
|
||||
const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
|
||||
const messageIndex = this.cline.clineMessages.findIndex(
|
||||
(msg) => msg.ts && msg.ts >= timeCutoff,
|
||||
)
|
||||
const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(
|
||||
(msg) => msg.ts && msg.ts >= timeCutoff,
|
||||
)
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
|
||||
|
||||
|
||||
if (answer === "Just this message") {
|
||||
// Find the next user message first
|
||||
const nextUserMessage = this.cline.clineMessages
|
||||
.slice(messageIndex + 1)
|
||||
.find(msg => msg.type === "say" && msg.say === "user_feedback")
|
||||
|
||||
.find((msg) => msg.type === "say" && msg.say === "user_feedback")
|
||||
|
||||
// Handle UI messages
|
||||
if (nextUserMessage) {
|
||||
// Find absolute index of next user message
|
||||
const nextUserMessageIndex = this.cline.clineMessages.findIndex(msg => msg === nextUserMessage)
|
||||
const nextUserMessageIndex = this.cline.clineMessages.findIndex(
|
||||
(msg) => msg === nextUserMessage,
|
||||
)
|
||||
// Keep messages before current message and after next user message
|
||||
await this.cline.overwriteClineMessages([
|
||||
...this.cline.clineMessages.slice(0, messageIndex),
|
||||
...this.cline.clineMessages.slice(nextUserMessageIndex)
|
||||
...this.cline.clineMessages.slice(nextUserMessageIndex),
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current message
|
||||
await this.cline.overwriteClineMessages(
|
||||
this.cline.clineMessages.slice(0, messageIndex)
|
||||
this.cline.clineMessages.slice(0, messageIndex),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Handle API messages
|
||||
if (apiConversationHistoryIndex !== -1) {
|
||||
if (nextUserMessage && nextUserMessage.ts) {
|
||||
// Keep messages before current API message and after next user message
|
||||
await this.cline.overwriteApiConversationHistory([
|
||||
...this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
...this.cline.apiConversationHistory.filter(msg => msg.ts && msg.ts >= nextUserMessage.ts)
|
||||
...this.cline.apiConversationHistory.slice(
|
||||
0,
|
||||
apiConversationHistoryIndex,
|
||||
),
|
||||
...this.cline.apiConversationHistory.filter(
|
||||
(msg) => msg.ts && msg.ts >= nextUserMessage.ts,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current API message
|
||||
await this.cline.overwriteApiConversationHistory(
|
||||
this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex)
|
||||
this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (answer === "This and all subsequent messages") {
|
||||
// Delete this message and all that follow
|
||||
await this.cline.overwriteClineMessages(this.cline.clineMessages.slice(0, messageIndex))
|
||||
await this.cline.overwriteClineMessages(
|
||||
this.cline.clineMessages.slice(0, messageIndex),
|
||||
)
|
||||
if (apiConversationHistoryIndex !== -1) {
|
||||
await this.cline.overwriteApiConversationHistory(this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex))
|
||||
await this.cline.overwriteApiConversationHistory(
|
||||
this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.initClineWithHistoryItem(historyItem)
|
||||
}
|
||||
}
|
||||
@@ -890,12 +905,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "enhancePrompt":
|
||||
if (message.text) {
|
||||
try {
|
||||
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = await this.getState()
|
||||
|
||||
const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } =
|
||||
await this.getState()
|
||||
|
||||
// Try to get enhancement config first, fall back to current config
|
||||
let configToUse: ApiConfiguration = apiConfiguration
|
||||
if (enhancementApiConfigId) {
|
||||
const config = listApiConfigMeta?.find(c => c.id === enhancementApiConfigId)
|
||||
const config = listApiConfigMeta?.find((c) => c.id === enhancementApiConfigId)
|
||||
if (config?.name) {
|
||||
const loadedConfig = await this.configManager.LoadConfig(config.name)
|
||||
if (loadedConfig.apiProvider) {
|
||||
@@ -903,31 +919,49 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const enhancedPrompt = await enhancePrompt(configToUse, message.text, customPrompts?.enhance)
|
||||
|
||||
const getEnhancePrompt = (value: string | PromptComponent | undefined): string => {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
return enhance.prompt // Use the constant from modes.ts which we know is a string
|
||||
}
|
||||
const enhancedPrompt = await enhancePrompt(
|
||||
configToUse,
|
||||
message.text,
|
||||
getEnhancePrompt(customPrompts?.enhance),
|
||||
)
|
||||
await this.postMessageToWebview({
|
||||
type: "enhancedPrompt",
|
||||
text: enhancedPrompt
|
||||
text: enhancedPrompt,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error enhancing prompt:", error)
|
||||
vscode.window.showErrorMessage("Failed to enhance prompt")
|
||||
await this.postMessageToWebview({
|
||||
type: "enhancedPrompt"
|
||||
type: "enhancedPrompt",
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
case "getSystemPrompt":
|
||||
try {
|
||||
const { apiConfiguration, customPrompts, customInstructions, preferredLanguage, browserViewportSize, mcpEnabled } = await this.getState()
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ''
|
||||
const {
|
||||
apiConfiguration,
|
||||
customPrompts,
|
||||
customInstructions,
|
||||
preferredLanguage,
|
||||
browserViewportSize,
|
||||
mcpEnabled,
|
||||
} = await this.getState()
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
||||
|
||||
const mode = message.mode ?? codeMode
|
||||
const mode = message.mode ?? defaultModeSlug
|
||||
const instructions = await addCustomInstructions(
|
||||
{ customInstructions, customPrompts, preferredLanguage },
|
||||
cwd,
|
||||
mode
|
||||
mode,
|
||||
)
|
||||
|
||||
const systemPrompt = await SYSTEM_PROMPT(
|
||||
@@ -937,14 +971,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
undefined,
|
||||
browserViewportSize ?? "900x600",
|
||||
mode,
|
||||
customPrompts
|
||||
customPrompts,
|
||||
)
|
||||
const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt
|
||||
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "systemPrompt",
|
||||
text: fullPrompt,
|
||||
mode: message.mode
|
||||
mode: message.mode,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting system prompt:", error)
|
||||
@@ -958,7 +992,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
const commits = await searchCommits(message.query || "", cwd)
|
||||
await this.postMessageToWebview({
|
||||
type: "commitSearchResults",
|
||||
commits
|
||||
commits,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error searching commits:", error)
|
||||
@@ -970,9 +1004,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "upsertApiConfiguration":
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
await this.configManager.SaveConfig(message.text, message.apiConfiguration);
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
|
||||
await this.configManager.SaveConfig(message.text, message.apiConfiguration)
|
||||
let listApiConfig = await this.configManager.ListConfig()
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
this.updateApiConfiguration(message.apiConfiguration),
|
||||
@@ -991,18 +1025,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
try {
|
||||
const { oldName, newName } = message.values
|
||||
|
||||
await this.configManager.SaveConfig(newName, message.apiConfiguration);
|
||||
await this.configManager.SaveConfig(newName, message.apiConfiguration)
|
||||
await this.configManager.DeleteConfig(oldName)
|
||||
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
const config = listApiConfig?.find(c => c.name === newName);
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
let listApiConfig = await this.configManager.ListConfig()
|
||||
const config = listApiConfig?.find((c) => c.name === newName)
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", newName),
|
||||
])
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
await Promise.all([this.updateGlobalState("currentApiConfigName", newName)])
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
@@ -1014,9 +1046,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
case "loadApiConfiguration":
|
||||
if (message.text) {
|
||||
try {
|
||||
const apiConfig = await this.configManager.LoadConfig(message.text);
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
|
||||
const apiConfig = await this.configManager.LoadConfig(message.text)
|
||||
const listApiConfig = await this.configManager.ListConfig()
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
this.updateGlobalState("currentApiConfigName", message.text),
|
||||
@@ -1043,16 +1075,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.configManager.DeleteConfig(message.text);
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
|
||||
await this.configManager.DeleteConfig(message.text)
|
||||
const listApiConfig = await this.configManager.ListConfig()
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
// If this was the current config, switch to first available
|
||||
let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.LoadConfig(listApiConfig[0].name);
|
||||
const apiConfig = await this.configManager.LoadConfig(listApiConfig[0].name)
|
||||
await Promise.all([
|
||||
this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
|
||||
this.updateApiConfiguration(apiConfig),
|
||||
@@ -1068,7 +1100,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
break
|
||||
case "getListApiConfiguration":
|
||||
try {
|
||||
let listApiConfig = await this.configManager.ListConfig();
|
||||
let listApiConfig = await this.configManager.ListConfig()
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||
} catch (error) {
|
||||
@@ -1076,7 +1108,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||
}
|
||||
break
|
||||
case "experimentalDiffStrategy":
|
||||
case "experimentalDiffStrategy":
|
||||
await this.updateGlobalState("experimentalDiffStrategy", message.bool ?? false)
|
||||
// Update diffStrategy in current Cline instance if it exists
|
||||
if (this.cline) {
|
||||
@@ -1092,13 +1124,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
|
||||
// Update mode's default config
|
||||
const { mode } = await this.getState();
|
||||
const { mode } = await this.getState()
|
||||
if (mode) {
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName");
|
||||
const listApiConfig = await this.configManager.ListConfig();
|
||||
const config = listApiConfig?.find(c => c.name === currentApiConfigName);
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
const listApiConfig = await this.configManager.ListConfig()
|
||||
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
|
||||
if (config?.id) {
|
||||
await this.configManager.SetModeConfig(mode, config.id);
|
||||
await this.configManager.SetModeConfig(mode, config.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,7 +1202,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.storeSecret("mistralApiKey", mistralApiKey)
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(apiConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomInstructions(instructions?: string) {
|
||||
@@ -1241,11 +1273,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
// VSCode LM API
|
||||
private async getVsCodeLmModels() {
|
||||
try {
|
||||
const models = await vscode.lm.selectChatModels({});
|
||||
return models || [];
|
||||
const models = await vscode.lm.selectChatModels({})
|
||||
return models || []
|
||||
} catch (error) {
|
||||
console.error('Error fetching VS Code LM models:', error);
|
||||
return [];
|
||||
console.error("Error fetching VS Code LM models:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1335,10 +1367,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
|
||||
async readGlamaModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const glamaModelsFilePath = path.join(
|
||||
await this.ensureCacheDirectoryExists(),
|
||||
GlobalFileNames.glamaModels,
|
||||
)
|
||||
const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
|
||||
const fileExists = await fileExistsAtPath(glamaModelsFilePath)
|
||||
if (fileExists) {
|
||||
const fileContents = await fs.readFile(glamaModelsFilePath, "utf8")
|
||||
@@ -1348,10 +1377,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
|
||||
async refreshGlamaModels() {
|
||||
const glamaModelsFilePath = path.join(
|
||||
await this.ensureCacheDirectoryExists(),
|
||||
GlobalFileNames.glamaModels,
|
||||
)
|
||||
const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
@@ -1386,7 +1412,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
*/
|
||||
if (response.data) {
|
||||
const rawModels = response.data;
|
||||
const rawModels = response.data
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
@@ -1554,7 +1580,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
uiMessagesFilePath: string
|
||||
apiConversationHistory: Anthropic.MessageParam[]
|
||||
}> {
|
||||
const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
|
||||
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
|
||||
const historyItem = history.find((item) => item.id === id)
|
||||
if (historyItem) {
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
@@ -1619,7 +1645,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
// Remove the task from history
|
||||
const taskHistory = (await this.getGlobalState("taskHistory") as HistoryItem[]) || []
|
||||
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
|
||||
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
||||
await this.updateGlobalState("taskHistory", updatedTaskHistory)
|
||||
|
||||
@@ -1660,13 +1686,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mode,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
const allowedCommands = vscode.workspace
|
||||
.getConfiguration('roo-cline')
|
||||
.get<string[]>('allowedCommands') || []
|
||||
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
@@ -1689,7 +1713,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
soundVolume: soundVolume ?? 0.5,
|
||||
browserViewportSize: browserViewportSize ?? "900x600",
|
||||
screenshotQuality: screenshotQuality ?? 75,
|
||||
preferredLanguage: preferredLanguage ?? 'English',
|
||||
preferredLanguage: preferredLanguage ?? "English",
|
||||
writeDelayMs: writeDelayMs ?? 1000,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
@@ -1698,10 +1722,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
requestDelaySeconds: requestDelaySeconds ?? 5,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
mode: mode ?? codeMode,
|
||||
mode: mode ?? defaultModeSlug,
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
}
|
||||
}
|
||||
@@ -1818,7 +1842,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
modeApiConfigs,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
@@ -1880,7 +1904,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
||||
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
|
||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
||||
])
|
||||
|
||||
@@ -1950,49 +1974,51 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
writeDelayMs: writeDelayMs ?? 1000,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
mode: mode ?? codeMode,
|
||||
preferredLanguage: preferredLanguage ?? (() => {
|
||||
// Get VSCode's locale setting
|
||||
const vscodeLang = vscode.env.language;
|
||||
// Map VSCode locale to our supported languages
|
||||
const langMap: { [key: string]: string } = {
|
||||
'en': 'English',
|
||||
'ar': 'Arabic',
|
||||
'pt-br': 'Brazilian Portuguese',
|
||||
'cs': 'Czech',
|
||||
'fr': 'French',
|
||||
'de': 'German',
|
||||
'hi': 'Hindi',
|
||||
'hu': 'Hungarian',
|
||||
'it': 'Italian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'pl': 'Polish',
|
||||
'pt': 'Portuguese',
|
||||
'ru': 'Russian',
|
||||
'zh-cn': 'Simplified Chinese',
|
||||
'es': 'Spanish',
|
||||
'zh-tw': 'Traditional Chinese',
|
||||
'tr': 'Turkish'
|
||||
};
|
||||
// Return mapped language or default to English
|
||||
return langMap[vscodeLang.split('-')[0]] ?? 'English';
|
||||
})(),
|
||||
mode: mode ?? defaultModeSlug,
|
||||
preferredLanguage:
|
||||
preferredLanguage ??
|
||||
(() => {
|
||||
// Get VSCode's locale setting
|
||||
const vscodeLang = vscode.env.language
|
||||
// Map VSCode locale to our supported languages
|
||||
const langMap: { [key: string]: string } = {
|
||||
en: "English",
|
||||
ar: "Arabic",
|
||||
"pt-br": "Brazilian Portuguese",
|
||||
cs: "Czech",
|
||||
fr: "French",
|
||||
de: "German",
|
||||
hi: "Hindi",
|
||||
hu: "Hungarian",
|
||||
it: "Italian",
|
||||
ja: "Japanese",
|
||||
ko: "Korean",
|
||||
pl: "Polish",
|
||||
pt: "Portuguese",
|
||||
ru: "Russian",
|
||||
"zh-cn": "Simplified Chinese",
|
||||
es: "Spanish",
|
||||
"zh-tw": "Traditional Chinese",
|
||||
tr: "Turkish",
|
||||
}
|
||||
// Return mapped language or default to English
|
||||
return langMap[vscodeLang.split("-")[0]] ?? "English"
|
||||
})(),
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: requestDelaySeconds ?? 5,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
||||
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
|
||||
const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
|
||||
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
|
||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user