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 e291197..a31c02a 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 05f33ba..f06dff3 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 0a32eef..7f89c43 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 1f977a4..24dc72c 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 0000000..e58a82f --- /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 332759a..37cc8c0 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 fa3b84f..5df7d03 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 24dc72c..0754c1a 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 58ec513..08a95ae 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 37cc8c0..719f38d 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 69cc518..a226beb 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 881c060..d80e9e4 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 0a32eef..76543f3 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 0fb9ad1..6042f41 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 0000000..88d64e6 --- /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 0000000..1cebabb --- /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 e291197..5ef3c08 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 2f46456..f868d17 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 b6f15d2..43ed56c 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 9eb6f82..ef09c03 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 3876c59..80eb793 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 f115bc6..fa3b84f 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 6083a56..5ca6e94 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 0c69367..0000000 --- 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 1ec109f..ffa340e 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 0a32eef..563184a 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 3e3bbd7..d72d856 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 d80e9e4..7146e47 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 7146e47..6086f3a 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 6086f3a..d097162 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}