From 3aa5b3f9befe2eeb69d556958e5379dd254a5cf1 Mon Sep 17 00:00:00 2001 From: Nathan Apter Date: Wed, 29 Jan 2025 10:21:36 -0500 Subject: [PATCH 01/13] Allow the user to send context with approval or rejection --- src/core/Cline.ts | 30 +++++++++--------------------- src/core/prompts/responses.ts | 3 +++ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index e2911976d3..a31c02a2ef 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1085,35 +1085,23 @@ export class Cline { const askApproval = async (type: ClineAsk, partialMessage?: string) => { const { response, text, images } = await this.ask(type, partialMessage, false) if (response !== "yesButtonClicked") { - if (response === "messageResponse") { + // Handle both messageResponse and noButtonClicked with text + if (text) { await this.say("user_feedback", text, images) pushToolResult( formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images), ) - // this.userMessageContent.push({ - // type: "text", - // text: `${toolDescription()}`, - // }) - // this.toolResults.push({ - // type: "tool_result", - // tool_use_id: toolUseId, - // content: this.formatToolResponseWithImages( - // await this.formatToolDeniedFeedback(text), - // images - // ), - // }) - this.didRejectTool = true - return false + } else { + pushToolResult(formatResponse.toolDenied()) } - pushToolResult(formatResponse.toolDenied()) - // this.toolResults.push({ - // type: "tool_result", - // tool_use_id: toolUseId, - // content: await this.formatToolDenied(), - // }) this.didRejectTool = true return false } + // Handle yesButtonClicked with text + if (text) { + await this.say("user_feedback", text, images) + pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) + } return true } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 05f33ba71a..f06dff3d88 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -8,6 +8,9 @@ export const formatResponse = { toolDeniedWithFeedback: (feedback?: string) => `The user denied this operation and provided the following feedback:\n\n${feedback}\n`, + toolApprovedWithFeedback: (feedback?: string) => + `The user approved this operation and provided the following context:\n\n${feedback}\n`, + toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, noToolsUsed: () => From fe82500e94f9b055df6bb84a4e7c56f2e6ac3d9c Mon Sep 17 00:00:00 2001 From: Nathan Apter Date: Wed, 29 Jan 2025 15:17:30 -0500 Subject: [PATCH 02/13] Send the text when the buttons are clicked --- webview-ui/src/components/chat/ChatView.tsx | 144 +++++++++++++------- 1 file changed, 92 insertions(+), 52 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0a32eef77a..7f89c434e2 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -337,56 +337,96 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie /* This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension. */ - const handlePrimaryButtonClick = useCallback(() => { - switch (clineAsk) { - case "api_req_failed": - case "command": - case "command_output": - case "tool": - case "browser_action_launch": - case "use_mcp_server": - case "resume_task": - case "mistake_limit_reached": - vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" }) - break - case "completion_result": - case "resume_completed_task": - // extension waiting for feedback. but we can just present a new task button - startNewTask() - break - } - setTextAreaDisabled(true) - setClineAsk(undefined) - setEnableButtons(false) - disableAutoScrollRef.current = false - }, [clineAsk, startNewTask]) + const handlePrimaryButtonClick = useCallback( + (text?: string, images?: string[]) => { + const trimmedInput = text?.trim() + switch (clineAsk) { + case "api_req_failed": + case "command": + case "command_output": + case "tool": + case "browser_action_launch": + case "use_mcp_server": + case "resume_task": + case "mistake_limit_reached": + // Only send text/images if they exist + if (trimmedInput || (images && images.length > 0)) { + vscode.postMessage({ + type: "askResponse", + askResponse: "yesButtonClicked", + text: trimmedInput, + images: images, + }) + } else { + vscode.postMessage({ + type: "askResponse", + askResponse: "yesButtonClicked", + }) + } + // Clear input state after sending + setInputValue("") + setSelectedImages([]) + break + case "completion_result": + case "resume_completed_task": + // extension waiting for feedback. but we can just present a new task button + startNewTask() + break + } + setTextAreaDisabled(true) + setClineAsk(undefined) + setEnableButtons(false) + disableAutoScrollRef.current = false + }, + [clineAsk, startNewTask], + ) - const handleSecondaryButtonClick = useCallback(() => { - if (isStreaming) { - vscode.postMessage({ type: "cancelTask" }) - setDidClickCancel(true) - return - } + const handleSecondaryButtonClick = useCallback( + (text?: string, images?: string[]) => { + const trimmedInput = text?.trim() + if (isStreaming) { + vscode.postMessage({ type: "cancelTask" }) + setDidClickCancel(true) + return + } - switch (clineAsk) { - case "api_req_failed": - case "mistake_limit_reached": - case "resume_task": - startNewTask() - break - case "command": - case "tool": - case "browser_action_launch": - case "use_mcp_server": - // responds to the API with a "This operation failed" and lets it try again - vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" }) - break - } - setTextAreaDisabled(true) - setClineAsk(undefined) - setEnableButtons(false) - disableAutoScrollRef.current = false - }, [clineAsk, startNewTask, isStreaming]) + switch (clineAsk) { + case "api_req_failed": + case "mistake_limit_reached": + case "resume_task": + startNewTask() + break + case "command": + case "tool": + case "browser_action_launch": + case "use_mcp_server": + // Only send text/images if they exist + if (trimmedInput || (images && images.length > 0)) { + vscode.postMessage({ + type: "askResponse", + askResponse: "noButtonClicked", + text: trimmedInput, + images: images, + }) + } else { + // responds to the API with a "This operation failed" and lets it try again + vscode.postMessage({ + type: "askResponse", + askResponse: "noButtonClicked", + }) + } + // Clear input state after sending + setInputValue("") + setSelectedImages([]) + break + } + setTextAreaDisabled(true) + setClineAsk(undefined) + setEnableButtons(false) + disableAutoScrollRef.current = false + }, + [clineAsk, startNewTask, isStreaming], + ) const handleTaskCloseButtonClick = useCallback(() => { startNewTask() @@ -430,10 +470,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie handleSendMessage(message.text ?? "", message.images ?? []) break case "primaryButtonClick": - handlePrimaryButtonClick() + handlePrimaryButtonClick(message.text ?? "", message.images ?? []) break case "secondaryButtonClick": - handleSecondaryButtonClick() + handleSecondaryButtonClick(message.text ?? "", message.images ?? []) break } } @@ -1038,7 +1078,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flex: secondaryButtonText ? 1 : 2, marginRight: secondaryButtonText ? "6px" : "0", }} - onClick={handlePrimaryButtonClick}> + onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}> {primaryButtonText} )} @@ -1050,7 +1090,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flex: isStreaming ? 2 : 1, marginLeft: isStreaming ? 0 : "6px", }} - onClick={handleSecondaryButtonClick}> + onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}> {isStreaming ? "Cancel" : secondaryButtonText} )} From 35a7e433f22f064379278980be559ef4f8d43aab Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 16:26:44 +0700 Subject: [PATCH 03/13] refactor: centralize editor utilities and unify command handling - Create EditorUtils class to centralize shared editor functionality - Remove duplicated code between CodeActionProvider and command handlers - Improve command handling to work consistently for both code actions and direct commands - Add better type safety and error handling for editor operations --- src/core/CodeActionProvider.ts | 112 ++------------------------ src/core/EditorUtils.ts | 141 +++++++++++++++++++++++++++++++++ src/extension.ts | 48 ++++++----- 3 files changed, 176 insertions(+), 125 deletions(-) create mode 100644 src/core/EditorUtils.ts diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 1f977a4c02..24dc72c506 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" -import * as path from "path" import { ClineProvider } from "./webview/ClineProvider" +import { EditorUtils } from "./EditorUtils" export const ACTION_NAMES = { EXPLAIN: "Roo Code: Explain Code", @@ -14,100 +14,12 @@ const COMMAND_IDS = { IMPROVE: "roo-cline.improveCode", } as const -interface DiagnosticData { - message: string - severity: vscode.DiagnosticSeverity - code?: string | number | { value: string | number; target: vscode.Uri } - source?: string - range: vscode.Range -} - -interface EffectiveRange { - range: vscode.Range - text: string -} - export class CodeActionProvider implements vscode.CodeActionProvider { public static readonly providedCodeActionKinds = [ vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.RefactorRewrite, ] - // Cache file paths for performance - private readonly filePathCache = new WeakMap() - - private getEffectiveRange( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - ): EffectiveRange | null { - try { - const selectedText = document.getText(range) - if (selectedText) { - return { range, text: selectedText } - } - - const currentLine = document.lineAt(range.start.line) - if (!currentLine.text.trim()) { - return null - } - - // Optimize range creation by checking bounds first - const startLine = Math.max(0, currentLine.lineNumber - 1) - const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) - - // Only create new positions if needed - const effectiveRange = new vscode.Range( - startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), - endLine === currentLine.lineNumber - ? range.end - : new vscode.Position(endLine, document.lineAt(endLine).text.length), - ) - - return { - range: effectiveRange, - text: document.getText(effectiveRange), - } - } catch (error) { - console.error("Error getting effective range:", error) - return null - } - } - - private getFilePath(document: vscode.TextDocument): string { - // Check cache first - let filePath = this.filePathCache.get(document) - if (filePath) { - return filePath - } - - try { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) - if (!workspaceFolder) { - filePath = document.uri.fsPath - } else { - const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) - filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath - } - - // Cache the result - this.filePathCache.set(document, filePath) - return filePath - } catch (error) { - console.error("Error getting file path:", error) - return document.uri.fsPath - } - } - - private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { - return { - message: diagnostic.message, - severity: diagnostic.severity, - code: diagnostic.code, - source: diagnostic.source, - range: diagnostic.range, // Reuse the range object - } - } - private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction { const action = new vscode.CodeAction(title, kind) action.command = { command, title, arguments: args } @@ -126,32 +38,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ] } - private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - // Optimize range intersection check - return !( - range2.end.line < range1.start.line || - range2.start.line > range1.end.line || - (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || - (range2.start.line === range1.end.line && range2.start.character > range1.end.character) - ) - } - public provideCodeActions( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { try { - const effectiveRange = this.getEffectiveRange(document, range) + const effectiveRange = EditorUtils.getEffectiveRange(document, range) if (!effectiveRange) { return [] } - const filePath = this.getFilePath(document) + const filePath = EditorUtils.getFilePath(document) const actions: vscode.CodeAction[] = [] - // Create actions using helper method - // Add explain actions actions.push( ...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ filePath, @@ -159,14 +59,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ]), ) - // Only process diagnostics if they exist if (context.diagnostics.length > 0) { const relevantDiagnostics = context.diagnostics.filter((d) => - this.hasIntersectingRange(effectiveRange.range, d.range), + EditorUtils.hasIntersectingRange(effectiveRange.range, d.range), ) if (relevantDiagnostics.length > 0) { - const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData) + const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData) actions.push( ...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ filePath, @@ -177,7 +76,6 @@ export class CodeActionProvider implements vscode.CodeActionProvider { } } - // Add improve actions actions.push( ...this.createActionPair( ACTION_NAMES.IMPROVE, diff --git a/src/core/EditorUtils.ts b/src/core/EditorUtils.ts new file mode 100644 index 0000000000..e58a82f18f --- /dev/null +++ b/src/core/EditorUtils.ts @@ -0,0 +1,141 @@ +import * as vscode from "vscode" +import * as path from "path" + +export interface EffectiveRange { + range: vscode.Range + text: string +} + +export interface DiagnosticData { + message: string + severity: vscode.DiagnosticSeverity + code?: string | number | { value: string | number; target: vscode.Uri } + source?: string + range: vscode.Range +} + +export interface EditorContext { + filePath: string + selectedText: string + diagnostics?: DiagnosticData[] +} + +export class EditorUtils { + // Cache file paths for performance + private static readonly filePathCache = new WeakMap() + + static getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + ): EffectiveRange | null { + try { + const selectedText = document.getText(range) + if (selectedText) { + return { range, text: selectedText } + } + + const currentLine = document.lineAt(range.start.line) + if (!currentLine.text.trim()) { + return null + } + + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1) + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) + + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber + ? range.end + : new vscode.Position(endLine, document.lineAt(endLine).text.length), + ) + + return { + range: effectiveRange, + text: document.getText(effectiveRange), + } + } catch (error) { + console.error("Error getting effective range:", error) + return null + } + } + + static getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document) + if (filePath) { + return filePath + } + + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder) { + filePath = document.uri.fsPath + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) + filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath + } + + // Cache the result + this.filePathCache.set(document, filePath) + return filePath + } catch (error) { + console.error("Error getting file path:", error) + return document.uri.fsPath + } + } + + static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range, + } + } + + static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ) + } + + static getEditorContext(editor?: vscode.TextEditor): EditorContext | null { + try { + if (!editor) { + editor = vscode.window.activeTextEditor + } + if (!editor) { + return null + } + + const document = editor.document + const selection = editor.selection + const effectiveRange = this.getEffectiveRange(document, selection) + + if (!effectiveRange) { + return null + } + + const filePath = this.getFilePath(document) + const diagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range)) + .map(this.createDiagnosticData) + + return { + filePath, + selectedText: effectiveRange.text, + ...(diagnostics.length > 0 ? { diagnostics } : {}), + } + } catch (error) { + console.error("Error getting editor context:", error) + return null + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 332759a381..37cc8c06eb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" +import { EditorUtils } from "./core/EditorUtils" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -178,26 +179,37 @@ export function activate(context: vscode.ExtensionContext) { let userInput: string | undefined context.subscriptions.push( - vscode.commands.registerCommand( - command, - async (filePath: string, selectedText: string, diagnostics?: any[]) => { - if (inputPrompt) { - userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder, - }) - } + vscode.commands.registerCommand(command, async (...args: any[]) => { + if (inputPrompt) { + userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) + } - const params = { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}), - ...(userInput ? { userInput } : {}), - } + // Handle both code action and direct command cases + let filePath: string + let selectedText: string + let diagnostics: any[] | undefined - await ClineProvider.handleCodeAction(command, promptType, params) - }, - ), + if (args.length > 1) { + // Called from code action + ;[filePath, selectedText, diagnostics] = args + } else { + // Called directly from command palette + const context = EditorUtils.getEditorContext() + if (!context) return + ;({ filePath, selectedText, diagnostics } = context) + } + + const params = { + ...{ filePath, selectedText }, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}), + } + + await ClineProvider.handleCodeAction(command, promptType, params) + }), ) } From 2e561496203b78591412f7b4b9604a3f5e4250cc Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 17:23:12 +0700 Subject: [PATCH 04/13] feat: add 'Add To Context' code action - Add new command registration and menu item - Add new code action type and command ID - Add support prompt config for adding code to context - Add message handling in webview for setting chat box content - Add logic to append selected code to existing chat input --- package.json | 10 ++++++++++ src/core/CodeActionProvider.ts | 20 +++++++++++++++++++- src/core/webview/ClineProvider.ts | 10 ++++++++++ src/extension.ts | 7 ++++--- src/shared/ExtensionMessage.ts | 2 +- src/shared/support-prompt.ts | 19 ++++++++++++++----- webview-ui/src/components/chat/ChatView.tsx | 18 ++++++++++++++++++ 7 files changed, 76 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index fa3b84ff82..5df7d0323d 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,11 @@ "command": "roo-cline.improveCode", "title": "Roo Code: Improve Code", "category": "Roo Code" + }, + { + "command": "roo-cline.addToContext", + "title": "Roo Code: Add To Context", + "category": "Roo Code" } ], "menus": { @@ -136,6 +141,11 @@ "command": "roo-cline.improveCode", "when": "editorHasSelection", "group": "Roo Code@3" + }, + { + "command": "roo-cline.addToContext", + "when": "editorHasSelection", + "group": "Roo Code@4" } ], "view/title": [ diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 24dc72c506..0754c1a7e2 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,17 +1,19 @@ import * as vscode from "vscode" -import { ClineProvider } from "./webview/ClineProvider" import { EditorUtils } from "./EditorUtils" export const ACTION_NAMES = { EXPLAIN: "Roo Code: Explain Code", FIX: "Roo Code: Fix Code", + FIX_LOGIC: "Roo Code: Fix Logic", IMPROVE: "Roo Code: Improve Code", + ADD_TO_CONTEXT: "Roo Code: Add to Context", } as const const COMMAND_IDS = { EXPLAIN: "roo-cline.explainCode", FIX: "roo-cline.fixCode", IMPROVE: "roo-cline.improveCode", + ADD_TO_CONTEXT: "roo-cline.addToContext", } as const export class CodeActionProvider implements vscode.CodeActionProvider { @@ -74,6 +76,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ]), ) } + } else { + actions.push( + ...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ + filePath, + effectiveRange.text, + ]), + ) } actions.push( @@ -85,6 +94,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ), ) + actions.push( + this.createAction( + ACTION_NAMES.ADD_TO_CONTEXT, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.ADD_TO_CONTEXT, + [filePath, effectiveRange.text], + ), + ) + return actions } catch (error) { console.error("Error providing code actions:", error) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 58ec513529..08a95ae44b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -238,6 +238,16 @@ export class ClineProvider implements vscode.WebviewViewProvider { const prompt = supportPrompt.create(promptType, params, customSupportPrompts) + if (command.endsWith("addToContext")) { + await visibleProvider.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: prompt, + }) + + return + } + if (visibleProvider.cline && command.endsWith("InCurrentTask")) { await visibleProvider.postMessageToWebview({ type: "invoke", diff --git a/src/extension.ts b/src/extension.ts index 37cc8c06eb..719f38d5e8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -172,7 +172,6 @@ export function activate(context: vscode.ExtensionContext) { context: vscode.ExtensionContext, command: string, promptType: keyof typeof ACTION_NAMES, - inNewTask: boolean, inputPrompt?: string, inputPlaceholder?: string, ) => { @@ -222,10 +221,10 @@ export function activate(context: vscode.ExtensionContext) { inputPlaceholder?: string, ) => { // Register new task version - registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder) + registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder) // Register current task version - registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder) + registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder) } // Register code action commands @@ -253,6 +252,8 @@ export function activate(context: vscode.ExtensionContext) { "E.g. Focus on performance optimization", ) + registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT") + return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 69cc5187f8..a226beb802 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -50,7 +50,7 @@ export interface ExtensionMessage { | "historyButtonClicked" | "promptsButtonClicked" | "didBecomeVisible" - invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" + invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage" state?: ExtensionState images?: string[] ollamaModels?: string[] diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 881c060057..d80e9e4014 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string => } } - // Replace any remaining user_input placeholders with empty string - result = result.replaceAll("${userInput}", "") + // Replace any remaining placeholders with empty strings + result = result.replaceAll(/\${[^}]*}/g, "") return result } @@ -42,7 +42,7 @@ const supportPromptConfigs: Record = { EXPLAIN: { label: "Explain Code", description: - "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).", + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Explain the following code from file path @/\${filePath}: \${userInput} @@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including FIX: { label: "Fix Issues", description: - "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).", + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -76,7 +76,7 @@ Please: IMPROVE: { label: "Improve Code", description: - "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).", + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", template: `Improve the following code from file path @/\${filePath}: \${userInput} @@ -92,6 +92,15 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement.`, }, + ADD_TO_CONTEXT: { + label: "Add to Context", + description: + "Add context to your current task or conversation. Useful for providing additional information or clarifications. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + template: `@/\${filePath}: +\`\`\` +\${selectedText} +\`\`\``, + }, } as const type SupportPromptType = keyof typeof supportPromptConfigs diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0a32eef77a..76543f3b61 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -330,6 +330,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie [messages.length, clineAsk], ) + const handleSetChatBoxMessage = useCallback( + (text: string, images: string[]) => { + // Avoid nested template literals by breaking down the logic + let newValue = text + if (inputValue !== "") { + newValue = inputValue + " " + text + } + + setInputValue(newValue) + setSelectedImages([...selectedImages, ...images]) + }, + [inputValue, selectedImages], + ) + const startNewTask = useCallback(() => { vscode.postMessage({ type: "clearTask" }) }, []) @@ -429,6 +443,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie case "sendMessage": handleSendMessage(message.text ?? "", message.images ?? []) break + case "setChatBoxMessage": + handleSetChatBoxMessage(message.text ?? "", message.images ?? []) + break case "primaryButtonClick": handlePrimaryButtonClick() break @@ -444,6 +461,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie textAreaDisabled, enableButtons, handleSendMessage, + handleSetChatBoxMessage, handlePrimaryButtonClick, handleSecondaryButtonClick, ], From bb5d506679e36bf809b58e9f4420504186841bcd Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 30 Jan 2025 17:42:11 +0700 Subject: [PATCH 05/13] refactor: extract editor utilities into EditorUtils module and add tests --- src/core/__tests__/CodeActionProvider.test.ts | 111 +++++++----------- src/core/__tests__/EditorUtils.test.ts | 75 ++++++++++++ 2 files changed, 120 insertions(+), 66 deletions(-) create mode 100644 src/core/__tests__/EditorUtils.test.ts diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts index 0fb9ad1aa1..6042f41b2b 100644 --- a/src/core/__tests__/CodeActionProvider.test.ts +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode" import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider" +import { EditorUtils } from "../EditorUtils" // Mock VSCode API jest.mock("vscode", () => ({ @@ -16,13 +17,6 @@ jest.mock("vscode", () => ({ start: { line: startLine, character: startChar }, end: { line: endLine, character: endChar }, })), - Position: jest.fn().mockImplementation((line, character) => ({ - line, - character, - })), - workspace: { - getWorkspaceFolder: jest.fn(), - }, DiagnosticSeverity: { Error: 0, Warning: 1, @@ -31,6 +25,16 @@ jest.mock("vscode", () => ({ }, })) +// Mock EditorUtils +jest.mock("../EditorUtils", () => ({ + EditorUtils: { + getEffectiveRange: jest.fn(), + getFilePath: jest.fn(), + hasIntersectingRange: jest.fn(), + createDiagnosticData: jest.fn(), + }, +})) + describe("CodeActionProvider", () => { let provider: CodeActionProvider let mockDocument: any @@ -55,68 +59,32 @@ describe("CodeActionProvider", () => { mockContext = { diagnostics: [], } - }) - describe("getEffectiveRange", () => { - it("should return selected text when available", () => { - mockDocument.getText.mockReturnValue("selected text") - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - - expect(result).toEqual({ - range: mockRange, - text: "selected text", - }) - }) - - it("should return null for empty line", () => { - mockDocument.getText.mockReturnValue("") - mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - - expect(result).toBeNull() - }) - }) - - describe("getFilePath", () => { - it("should return relative path when in workspace", () => { - const mockWorkspaceFolder = { - uri: { fsPath: "/test" }, - } - ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) - - const result = (provider as any).getFilePath(mockDocument) - - expect(result).toBe("file.ts") - }) - - it("should return absolute path when not in workspace", () => { - ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) - - const result = (provider as any).getFilePath(mockDocument) - - expect(result).toBe("/test/file.ts") + // Setup default EditorUtils mocks + ;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({ + range: mockRange, + text: "test code", }) + ;(EditorUtils.getFilePath as jest.Mock).mockReturnValue("/test/file.ts") + ;(EditorUtils.hasIntersectingRange as jest.Mock).mockReturnValue(true) + ;(EditorUtils.createDiagnosticData as jest.Mock).mockImplementation((d) => d) }) describe("provideCodeActions", () => { - beforeEach(() => { - mockDocument.getText.mockReturnValue("test code") - mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 }) - }) - - it("should provide explain and improve actions by default", () => { + it("should provide explain, improve, fix logic, and add to context actions by default", () => { const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) - expect(actions).toHaveLength(4) + expect(actions).toHaveLength(7) // 2 explain + 2 fix logic + 2 improve + 1 add to context expect((actions as any)[0].title).toBe(`${ACTION_NAMES.EXPLAIN} in New Task`) expect((actions as any)[1].title).toBe(`${ACTION_NAMES.EXPLAIN} in Current Task`) - expect((actions as any)[2].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`) - expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`) + expect((actions as any)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`) + expect((actions as any)[3].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in Current Task`) + expect((actions as any)[4].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`) + expect((actions as any)[5].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`) + expect((actions as any)[6].title).toBe(ACTION_NAMES.ADD_TO_CONTEXT) }) - it("should provide fix action when diagnostics exist", () => { + it("should provide fix action instead of fix logic when diagnostics exist", () => { mockContext.diagnostics = [ { message: "test error", @@ -127,22 +95,33 @@ describe("CodeActionProvider", () => { const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) - expect(actions).toHaveLength(6) + expect(actions).toHaveLength(7) // 2 explain + 2 fix + 2 improve + 1 add to context expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in New Task`)).toBe(true) expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in Current Task`)).toBe(true) + expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in New Task`)).toBe(false) + expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in Current Task`)).toBe( + false, + ) }) - it("should handle errors gracefully", () => { - const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) - mockDocument.getText.mockImplementation(() => { - throw new Error("Test error") - }) - mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 }) + it("should return empty array when no effective range", () => { + ;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null) const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) expect(actions).toEqual([]) - expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error)) + }) + + it("should handle errors gracefully", () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + ;(EditorUtils.getEffectiveRange as jest.Mock).mockImplementation(() => { + throw new Error("Test error") + }) + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toEqual([]) + expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error)) consoleErrorSpy.mockRestore() }) diff --git a/src/core/__tests__/EditorUtils.test.ts b/src/core/__tests__/EditorUtils.test.ts new file mode 100644 index 0000000000..88d64e6da8 --- /dev/null +++ b/src/core/__tests__/EditorUtils.test.ts @@ -0,0 +1,75 @@ +import * as vscode from "vscode" +import { EditorUtils } from "../EditorUtils" + +// Mock VSCode API +jest.mock("vscode", () => ({ + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character, + })), + workspace: { + getWorkspaceFolder: jest.fn(), + }, +})) + +describe("EditorUtils", () => { + let mockDocument: any + + beforeEach(() => { + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: "/test/file.ts" }, + } + }) + + describe("getEffectiveRange", () => { + it("should return selected text when available", () => { + const mockRange = new vscode.Range(0, 0, 0, 10) + mockDocument.getText.mockReturnValue("selected text") + + const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) + + expect(result).toEqual({ + range: mockRange, + text: "selected text", + }) + }) + + it("should return null for empty line", () => { + const mockRange = new vscode.Range(0, 0, 0, 10) + mockDocument.getText.mockReturnValue("") + mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) + + const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) + + expect(result).toBeNull() + }) + }) + + describe("getFilePath", () => { + it("should return relative path when in workspace", () => { + const mockWorkspaceFolder = { + uri: { fsPath: "/test" }, + } + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) + + const result = EditorUtils.getFilePath(mockDocument) + + expect(result).toBe("file.ts") + }) + + it("should return absolute path when not in workspace", () => { + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) + + const result = EditorUtils.getFilePath(mockDocument) + + expect(result).toBe("/test/file.ts") + }) + }) +}) From e10004d865a8e7e8400705778739880d665ee9d2 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 30 Jan 2025 11:35:57 -0500 Subject: [PATCH 06/13] Use an exponential delay for API retries --- .changeset/tame-walls-kiss.md | 5 +++++ src/core/Cline.ts | 26 +++++++++++++++-------- src/core/__tests__/Cline.test.ts | 36 ++++++++++++++------------------ 3 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 .changeset/tame-walls-kiss.md diff --git a/.changeset/tame-walls-kiss.md b/.changeset/tame-walls-kiss.md new file mode 100644 index 0000000000..1cebabb2bc --- /dev/null +++ b/.changeset/tame-walls-kiss.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Use an exponential backoff for API retries diff --git a/src/core/Cline.ts b/src/core/Cline.ts index e2911976d3..5ef3c0862c 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -793,7 +793,7 @@ export class Cline { } } - async *attemptApiRequest(previousApiReqIndex: number): ApiStream { + async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream { let mcpHub: McpHub | undefined const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } = @@ -887,21 +887,29 @@ export class Cline { // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. if (alwaysApproveResubmit) { const errorMsg = error.message ?? "Unknown error" - const requestDelay = requestDelaySeconds || 5 - // Automatically retry with delay - // Show countdown timer in error color - for (let i = requestDelay; i > 0; i--) { + const baseDelay = requestDelaySeconds || 5 + const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) + + // Show countdown timer with exponential backoff + for (let i = exponentialDelay; i > 0; i--) { await this.say( "api_req_retry_delayed", - `${errorMsg}\n\nRetrying in ${i} seconds...`, + `${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`, undefined, true, ) await delay(1000) } - await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false) - // delegate generator output from the recursive call - yield* this.attemptApiRequest(previousApiReqIndex) + + await this.say( + "api_req_retry_delayed", + `${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`, + undefined, + false, + ) + + // delegate generator output from the recursive call with incremented retry count + yield* this.attemptApiRequest(previousApiReqIndex, retryAttempt + 1) return } else { const { response } = await this.ask( diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index 2f46456de2..f868d17d18 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -730,25 +730,19 @@ describe("Cline", () => { const iterator = cline.attemptApiRequest(0) await iterator.next() + // Calculate expected delay for first retry + const baseDelay = 3 // from requestDelaySeconds + // Verify countdown messages - expect(saySpy).toHaveBeenCalledWith( - "api_req_retry_delayed", - expect.stringContaining("Retrying in 3 seconds"), - undefined, - true, - ) - expect(saySpy).toHaveBeenCalledWith( - "api_req_retry_delayed", - expect.stringContaining("Retrying in 2 seconds"), - undefined, - true, - ) - expect(saySpy).toHaveBeenCalledWith( - "api_req_retry_delayed", - expect.stringContaining("Retrying in 1 seconds"), - undefined, - true, - ) + for (let i = baseDelay; i > 0; i--) { + expect(saySpy).toHaveBeenCalledWith( + "api_req_retry_delayed", + expect.stringContaining(`Retrying in ${i} seconds`), + undefined, + true, + ) + } + expect(saySpy).toHaveBeenCalledWith( "api_req_retry_delayed", expect.stringContaining("Retrying now"), @@ -757,12 +751,14 @@ describe("Cline", () => { ) // Verify delay was called correctly - expect(mockDelay).toHaveBeenCalledTimes(3) + expect(mockDelay).toHaveBeenCalledTimes(baseDelay) expect(mockDelay).toHaveBeenCalledWith(1000) // Verify error message content const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1] - expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`) + expect(errorMessage).toBe( + `${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`, + ) }) describe("loadContext", () => { From 161a6a5d9e1c6fc24802e5bee7a49dca734c02d9 Mon Sep 17 00:00:00 2001 From: Piotr Rogowski Date: Thu, 30 Jan 2025 23:43:15 +0100 Subject: [PATCH 07/13] Relax deepseek-r1 detection so it also includes distilled models --- src/api/providers/openrouter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index b6f15d2caf..43ed56c7f1 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -118,8 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { // Handle models based on deepseek-r1 if ( - this.getModel().id === "deepseek/deepseek-r1" || - this.getModel().id.startsWith("deepseek/deepseek-r1:") || + this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning" ) { // Recommended temperature for DeepSeek reasoning models From ce072d99d2fccdb5a1e2eff0f958cc8f651ac7e3 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 30 Jan 2025 22:29:57 -0500 Subject: [PATCH 08/13] Clearer modes prompt section --- src/core/prompts/sections/modes.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts index 9eb6f8254c..ef09c035c9 100644 --- a/src/core/prompts/sections/modes.ts +++ b/src/core/prompts/sections/modes.ts @@ -16,13 +16,17 @@ MODES ${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")} Custom modes will be referred to by their configured name property. -- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty: +- Custom modes can be configured by editing the custom modes file at '${customModesPath}'. The file gets created automatically on startup and should always exist. Make sure to read the latest contents before writing to it to avoid overwriting existing modes. + +- The following fields are required and must not be empty: * slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better. * name: The display name for the mode * roleDefinition: A detailed description of the mode's role and capabilities * groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files) -The customInstructions field is optional. +- The customInstructions field is optional. + +- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break." The file should follow this structure: { From 810bbb449f2dd0e93730d56de9126a4373471226 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 30 Jan 2025 22:39:21 -0500 Subject: [PATCH 09/13] Revert "chore: update eslint config and quiet lint output" This reverts commit 6db3ecf73e1250e03bc9b3485ab669d8f87f0686. --- .eslintrc.json | 52 ++++++++--------------------------------- package.json | 2 +- webview-ui/package.json | 2 +- 3 files changed, 12 insertions(+), 44 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3876c5986c..80eb793f5f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,56 +1,24 @@ { "root": true, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 2021, - "sourceType": "module", - "project": "./tsconfig.json" + "ecmaVersion": 6, + "sourceType": "module" }, "plugins": ["@typescript-eslint"], "rules": { - "@typescript-eslint/naming-convention": ["warn"], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": [ + "@typescript-eslint/naming-convention": [ "warn", { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" + "selector": "import", + "format": ["camelCase", "PascalCase"] } ], - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - "allowExpressions": true, - "allowTypedFunctionExpressions": true - } - ], - "@typescript-eslint/explicit-member-accessibility": [ - "warn", - { - "accessibility": "explicit" - } - ], - "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/semi": "off", + "eqeqeq": "warn", "no-throw-literal": "warn", - "semi": ["off", "always"], - "quotes": ["warn", "double", { "avoidEscape": true }], - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-var-requires": "warn", - "no-extra-semi": "warn", - "prefer-const": "warn", - "no-mixed-spaces-and-tabs": "warn", - "no-case-declarations": "warn", - "no-useless-escape": "warn", - "require-yield": "warn", - "no-empty": "warn", - "no-control-regex": "warn", - "@typescript-eslint/ban-ts-comment": "warn" + "semi": "off", + "react-hooks/exhaustive-deps": "off" }, - "env": { - "node": true, - "es2021": true - }, - "ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"] + "ignorePatterns": ["out", "dist", "**/*.d.ts"] } diff --git a/package.json b/package.json index f115bc621f..fa3b84ff82 100644 --- a/package.json +++ b/package.json @@ -214,7 +214,7 @@ "compile": "npm run check-types && npm run lint && node esbuild.js", "compile-tests": "tsc -p . --outDir out", "install:all": "npm install && cd webview-ui && npm install", - "lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui", + "lint": "eslint src --ext ts && npm run lint --prefix webview-ui", "package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production", "pretest": "npm run compile-tests && npm run compile && npm run lint", "start:webview": "cd webview-ui && npm run start", diff --git a/webview-ui/package.json b/webview-ui/package.json index 6083a56ccc..5ca6e945da 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -8,7 +8,7 @@ "build": "tsc && vite build", "preview": "vite preview", "test": "jest", - "lint": "eslint src --ext ts,tsx --quiet" + "lint": "eslint src --ext ts,tsx" }, "dependencies": { "@vscode/webview-ui-toolkit": "^1.4.0", From ab4d717211594c8354dc8d9fab1cb7fe8db98245 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 30 Jan 2025 23:00:52 -0500 Subject: [PATCH 10/13] Revert lint rules in webview-ui too --- webview-ui/.eslintrc.json | 40 ------------------- webview-ui/src/components/chat/ChatRow.tsx | 16 ++++---- webview-ui/src/components/chat/ChatView.tsx | 6 +-- .../src/components/welcome/WelcomeView.tsx | 2 +- 4 files changed, 12 insertions(+), 52 deletions(-) delete mode 100644 webview-ui/.eslintrc.json diff --git a/webview-ui/.eslintrc.json b/webview-ui/.eslintrc.json deleted file mode 100644 index 0c69367ccf..0000000000 --- a/webview-ui/.eslintrc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended" - ], - "parser": "@typescript-eslint/parser", - "plugins": ["react", "@typescript-eslint", "react-hooks"], - "rules": { - "react/react-in-jsx-scope": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "warn", - "react/display-name": "warn", - "no-case-declarations": "warn", - "react/no-unescaped-entities": "warn", - "react/jsx-key": "warn", - "no-extra-semi": "warn", - "@typescript-eslint/no-var-requires": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ] - }, - "settings": { - "react": { - "version": "detect" - } - }, - "env": { - "browser": true, - "es2021": true, - "node": true - } -} diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1ec109f930..ffa340e2f0 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -89,7 +89,7 @@ export const ChatRowContent = ({ } }, [isLast, message.say]) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { - if (message.text != null && message.say === "api_req_started") { + if (message.text && message.say === "api_req_started") { const info: ClineApiReqInfo = JSON.parse(message.text) return [info.cost, info.cancelReason, info.streamingFailedMessage] } @@ -183,26 +183,26 @@ export const ChatRowContent = ({ ) return [ - apiReqCancelReason != null ? ( + apiReqCancelReason !== null ? ( apiReqCancelReason === "user_cancelled" ? ( getIconSpan("error", cancelledColor) ) : ( getIconSpan("error", errorColor) ) - ) : cost != null ? ( + ) : cost !== null ? ( getIconSpan("check", successColor) ) : apiRequestFailedMessage ? ( getIconSpan("error", errorColor) ) : ( ), - apiReqCancelReason != null ? ( + apiReqCancelReason !== null ? ( apiReqCancelReason === "user_cancelled" ? ( API Request Cancelled ) : ( API Streaming Failed ) - ) : cost != null ? ( + ) : cost !== null ? ( API Request ) : apiRequestFailedMessage ? ( API Request Failed @@ -510,7 +510,7 @@ export const ChatRowContent = ({ style={{ ...headerStyle, marginBottom: - (cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage + (cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage ? 10 : 0, justifyContent: "space-between", @@ -524,13 +524,13 @@ export const ChatRowContent = ({
{icon} {title} - 0 ? 1 : 0 }}> + ${Number(cost || 0)?.toFixed(4)}
- {((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && ( + {((cost === null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && ( <>

{apiRequestFailedMessage || apiReqStreamingFailedMessage} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0a32eef77a..563184a129 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -275,7 +275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie return true } else { const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started") - if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") { + if (lastApiReqStarted && lastApiReqStarted.text && lastApiReqStarted.say === "api_req_started") { const cost = JSON.parse(lastApiReqStarted.text).cost if (cost === undefined) { // api request has not finished yet @@ -660,9 +660,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie if (message.say === "api_req_started") { // get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started") - if (lastApiReqStarted?.text != null) { + if (lastApiReqStarted?.text) { const info = JSON.parse(lastApiReqStarted.text) - const isCancelled = info.cancelReason != null + const isCancelled = info.cancelReason !== null if (isCancelled) { endBrowserSession() result.push(message) diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 3e3bbd7cd1..d72d856580 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -10,7 +10,7 @@ const WelcomeView = () => { const [apiErrorMessage, setApiErrorMessage] = useState(undefined) - const disableLetsGoButton = apiErrorMessage != null + const disableLetsGoButton = !!apiErrorMessage const handleSubmit = () => { vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) From 308eab05635fa90c88de6abb291652a8f5682817 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:15 -0500 Subject: [PATCH 11/13] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index d80e9e4014..7146e477e5 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -42,7 +42,7 @@ const supportPromptConfigs: Record = { EXPLAIN: { label: "Explain Code", description: - "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Explain the following code from file path @/\${filePath}: \${userInput} From 099447d06bc8dc5f48fa327a139cbaa9d843e04e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:20 -0500 Subject: [PATCH 12/13] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 7146e477e5..6086f3a9fd 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including FIX: { label: "Fix Issues", description: - "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} From 7614048ed46d160b2785147c5dcaafe2961226cb Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 31 Jan 2025 00:22:26 -0500 Subject: [PATCH 13/13] Update src/shared/support-prompt.ts --- src/shared/support-prompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 6086f3a9fd..d097162a4e 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -76,7 +76,7 @@ Please: IMPROVE: { label: "Improve Code", description: - "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).", + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).", template: `Improve the following code from file path @/\${filePath}: \${userInput}