This commit is contained in:
Matt Rubens 2025-01-23 09:04:13 -08:00
parent e97b9f9772
commit 7747bf0c6d
5 changed files with 406 additions and 413 deletions

View file

@ -40,7 +40,7 @@ Hot off the heels of **v3.0** introducing Code, Architect, and Ask chat modes, o
You can now tailor the **role definition** and **custom instructions** for every chat mode to perfectly fit your workflow. Want to adjust Architect mode to focus more on system scalability? Or tweak Ask mode for deeper research queries? Done. Plus, you can define these via **mode-specific `.clinerules-[mode]` files**. Youll find all of this in the new **Prompts** tab in the top menu.
The second big feature in this release is a complete revamp of **prompt enhancements**. This feature helps you craft messages to get even better results from Cline. Heres whats new:
The second big feature in this release is a complete revamp of **prompt enhancements**. This feature helps you craft messages to get even better results from Roo. Heres whats new:
- Works with **any provider** and API configuration, not just OpenRouter.
- Fully customizable prompts to match your unique needs.
@ -48,28 +48,26 @@ The second big feature in this release is a complete revamp of **prompt enhancem
Whether youre using GPT-4, other APIs, or switching configurations, this gives you total control over how your prompts are optimized.
As always, wed love to hear your thoughts and ideas! What features do you want to see in **v3.2**? Drop by https://www.reddit.com/r/roocline and join the discussion - we're building Roo Cline together. 🚀
## New in 3.0 - Chat Modes!
You can now choose between different prompts for Roo Cline to better suit your workflow. Heres whats available:
You can now choose between different prompts for Roo to better suit your workflow. Heres whats available:
- **Code:** (existing behavior) The default mode where Cline helps you write code and execute tasks.
- **Code:** (existing behavior) The default mode where Roo helps you write code and execute tasks.
- **Architect:** "You are Cline, a software architecture expert..." Ideal for thinking through high-level technical design and system architecture. Cant write code or run commands.
- **Architect:** "You are Roo, a software architecture expert..." Ideal for thinking through high-level technical design and system architecture. Cant write code or run commands.
- **Ask:** "You are Cline, a knowledgeable technical assistant..." Perfect for asking questions about the codebase or digging into concepts. Also cant write code or run commands.
- **Ask:** "You are Roo, a knowledgeable technical assistant..." Perfect for asking questions about the codebase or digging into concepts. Also cant write code or run commands.
**Switching Modes:**
Its super simple! Theres a dropdown in the bottom left of the chat input to switch modes. Right next to it, youll find a way to switch between the API configuration profiles associated with the current mode (configured on the settings screen).
**Why Add This?**
- It keeps Cline from being overly eager to jump into solving problems when you just want to think or ask questions.
- It keeps Roo from being overly eager to jump into solving problems when you just want to think or ask questions.
- Each mode remembers the API configuration you last used with it. For example, you can use more thoughtful models like OpenAI o1 for Architect and Ask, while sticking with Sonnet or DeepSeek for coding tasks.
- It builds on research suggesting better results when separating "thinking" from "coding," explained well in this very thoughtful [article](https://aider.chat/2024/09/26/architect.html) from aider.
Right now, switching modes is a manual process. In the future, wed love to give Cline the ability to suggest mode switches based on context. For now, wed really appreciate your feedback on this feature.
Right now, switching modes is a manual process. In the future, wed love to give Roo the ability to suggest mode switches based on context. For now, wed really appreciate your feedback on this feature.
---

View file

@ -72,102 +72,102 @@
"title": "New Task",
"icon": "$(add)"
},
{
"command": "roo-cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(server)"
},
{
"command": "roo-cline.promptsButtonClicked",
"title": "Prompts",
"icon": "$(notebook)"
},
{
"command": "roo-cline.historyButtonClicked",
"title": "History",
"icon": "$(history)"
},
{
"command": "roo-cline.popoutButtonClicked",
"title": "Open in Editor",
"icon": "$(link-external)"
},
{
"command": "roo-cline.settingsButtonClicked",
"title": "Settings",
"icon": "$(settings-gear)"
},
{
"command": "roo-cline.openInNewTab",
"title": "Open In New Tab",
"category": "Roo Code"
},
{
"command": "roo-cline.explainCode",
"title": "Explain Code",
"category": "Roo Cline"
},
{
"command": "roo-cline.fixCode",
"title": "Fix Code",
"category": "Roo Cline"
},
{
"command": "roo-cline.improveCode",
"title": "Improve Code",
"category": "Roo Cline"
}
{
"command": "roo-cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(server)"
},
{
"command": "roo-cline.promptsButtonClicked",
"title": "Prompts",
"icon": "$(notebook)"
},
{
"command": "roo-cline.historyButtonClicked",
"title": "History",
"icon": "$(history)"
},
{
"command": "roo-cline.popoutButtonClicked",
"title": "Open in Editor",
"icon": "$(link-external)"
},
{
"command": "roo-cline.settingsButtonClicked",
"title": "Settings",
"icon": "$(settings-gear)"
},
{
"command": "roo-cline.openInNewTab",
"title": "Open In New Tab",
"category": "Roo Code"
},
{
"command": "roo-cline.explainCode",
"title": "Explain Code",
"category": "Roo Code"
},
{
"command": "roo-cline.fixCode",
"title": "Fix Code",
"category": "Roo Code"
},
{
"command": "roo-cline.improveCode",
"title": "Improve Code",
"category": "Roo Code"
}
],
"menus": {
"editor/context": [
{
"command": "roo-cline.explainCode",
"when": "editorHasSelection",
"group": "Roo Cline@1"
},
{
"command": "roo-cline.fixCode",
"when": "editorHasSelection",
"group": "Roo Cline@2"
},
{
"command": "roo-cline.improveCode",
"when": "editorHasSelection",
"group": "Roo Cline@3"
}
],
"view/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider"
}
]
"editor/context": [
{
"command": "roo-cline.explainCode",
"when": "editorHasSelection",
"group": "Roo Code@1"
},
{
"command": "roo-cline.fixCode",
"when": "editorHasSelection",
"group": "Roo Code@2"
},
{
"command": "roo-cline.improveCode",
"when": "editorHasSelection",
"group": "Roo Code@3"
}
],
"view/title": [
{
"command": "roo-cline.plusButtonClicked",
"group": "navigation@1",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.promptsButtonClicked",
"group": "navigation@2",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.mcpButtonClicked",
"group": "navigation@3",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.historyButtonClicked",
"group": "navigation@4",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.popoutButtonClicked",
"group": "navigation@5",
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider"
}
]
},
"configuration": {
"title": "Roo Code",

View file

@ -1,181 +1,179 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as vscode from "vscode"
import * as path from "path"
export const ACTION_NAMES = {
EXPLAIN: 'Roo Cline: Explain Code',
FIX: 'Roo Cline: Fix Code',
IMPROVE: 'Roo Cline: Improve Code'
} as const;
EXPLAIN: "Roo Code: Explain Code",
FIX: "Roo Code: Fix Code",
IMPROVE: "Roo Code: Improve Code",
} as const
const COMMAND_IDS = {
EXPLAIN: 'roo-cline.explainCode',
FIX: 'roo-cline.fixCode',
IMPROVE: 'roo-cline.improveCode'
} as const;
EXPLAIN: "roo-cline.explainCode",
FIX: "roo-cline.fixCode",
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;
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;
range: vscode.Range
text: string
}
export class CodeActionProvider implements vscode.CodeActionProvider {
public static readonly providedCodeActionKinds = [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.RefactorRewrite,
];
public static readonly providedCodeActionKinds = [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.RefactorRewrite,
]
// Cache file paths for performance
private readonly filePathCache = new WeakMap<vscode.TextDocument, string>();
// Cache file paths for performance
private readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
private getEffectiveRange(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection
): EffectiveRange | null {
try {
const selectedText = document.getText(range);
if (selectedText) {
return { range, text: selectedText };
}
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;
}
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)
);
// 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)
return {
range: effectiveRange,
text: document.getText(effectiveRange)
};
} catch (error) {
console.error('Error getting effective range:', error);
return null;
}
}
// 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),
)
private getFilePath(document: vscode.TextDocument): string {
// Check cache first
let filePath = this.filePathCache.get(document);
if (filePath) {
return filePath;
}
return {
range: effectiveRange,
text: document.getText(effectiveRange),
}
} catch (error) {
console.error("Error getting effective range:", error)
return null
}
}
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;
}
private getFilePath(document: vscode.TextDocument): string {
// Check cache first
let filePath = this.filePathCache.get(document)
if (filePath) {
return filePath
}
// Cache the result
this.filePathCache.set(document, filePath);
return filePath;
} catch (error) {
console.error('Error getting file path:', error);
return document.uri.fsPath;
}
}
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
}
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
};
}
// Cache the result
this.filePathCache.set(document, filePath)
return filePath
} catch (error) {
console.error("Error getting file path:", error)
return document.uri.fsPath
}
}
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 };
return action;
}
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 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)
);
}
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 }
return action
}
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);
if (!effectiveRange) {
return [];
}
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)
)
}
const filePath = this.getFilePath(document);
const actions: vscode.CodeAction[] = [];
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)
if (!effectiveRange) {
return []
}
// Create actions using helper method
actions.push(this.createAction(
ACTION_NAMES.EXPLAIN,
vscode.CodeActionKind.QuickFix,
COMMAND_IDS.EXPLAIN,
[filePath, effectiveRange.text]
));
const filePath = this.getFilePath(document)
const actions: vscode.CodeAction[] = []
// Only process diagnostics if they exist
if (context.diagnostics.length > 0) {
const relevantDiagnostics = context.diagnostics.filter(d =>
this.hasIntersectingRange(effectiveRange.range, d.range)
);
// Create actions using helper method
actions.push(
this.createAction(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
filePath,
effectiveRange.text,
]),
)
if (relevantDiagnostics.length > 0) {
const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData);
actions.push(this.createAction(
ACTION_NAMES.FIX,
vscode.CodeActionKind.QuickFix,
COMMAND_IDS.FIX,
[filePath, effectiveRange.text, diagnosticMessages]
));
}
}
// Only process diagnostics if they exist
if (context.diagnostics.length > 0) {
const relevantDiagnostics = context.diagnostics.filter((d) =>
this.hasIntersectingRange(effectiveRange.range, d.range),
)
actions.push(this.createAction(
ACTION_NAMES.IMPROVE,
vscode.CodeActionKind.RefactorRewrite,
COMMAND_IDS.IMPROVE,
[filePath, effectiveRange.text]
));
if (relevantDiagnostics.length > 0) {
const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData)
actions.push(
this.createAction(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
filePath,
effectiveRange.text,
diagnosticMessages,
]),
)
}
}
return actions;
} catch (error) {
console.error('Error providing code actions:', error);
return [];
}
}
}
actions.push(
this.createAction(ACTION_NAMES.IMPROVE, vscode.CodeActionKind.RefactorRewrite, COMMAND_IDS.IMPROVE, [
filePath,
effectiveRange.text,
]),
)
return actions
} catch (error) {
console.error("Error providing code actions:", error)
return []
}
}
}

View file

@ -1,145 +1,147 @@
import * as vscode from 'vscode';
import { CodeActionProvider } from '../CodeActionProvider';
import * as vscode from "vscode"
import { CodeActionProvider } from "../CodeActionProvider"
// Mock VSCode API
jest.mock('vscode', () => ({
CodeAction: jest.fn().mockImplementation((title, kind) => ({
title,
kind,
command: undefined
})),
CodeActionKind: {
QuickFix: { value: 'quickfix' },
RefactorRewrite: { value: 'refactor.rewrite' }
},
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()
},
DiagnosticSeverity: {
Error: 0,
Warning: 1,
Information: 2,
Hint: 3
}
}));
jest.mock("vscode", () => ({
CodeAction: jest.fn().mockImplementation((title, kind) => ({
title,
kind,
command: undefined,
})),
CodeActionKind: {
QuickFix: { value: "quickfix" },
RefactorRewrite: { value: "refactor.rewrite" },
},
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(),
},
DiagnosticSeverity: {
Error: 0,
Warning: 1,
Information: 2,
Hint: 3,
},
}))
describe('CodeActionProvider', () => {
let provider: CodeActionProvider;
let mockDocument: any;
let mockRange: any;
let mockContext: any;
describe("CodeActionProvider", () => {
let provider: CodeActionProvider
let mockDocument: any
let mockRange: any
let mockContext: any
beforeEach(() => {
provider = new CodeActionProvider();
// Mock document
mockDocument = {
getText: jest.fn(),
lineAt: jest.fn(),
lineCount: 10,
uri: { fsPath: '/test/file.ts' }
};
beforeEach(() => {
provider = new CodeActionProvider()
// Mock range
mockRange = new vscode.Range(0, 0, 0, 10);
// Mock document
mockDocument = {
getText: jest.fn(),
lineAt: jest.fn(),
lineCount: 10,
uri: { fsPath: "/test/file.ts" },
}
// Mock context
mockContext = {
diagnostics: []
};
});
// Mock range
mockRange = new vscode.Range(0, 0, 0, 10)
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'
});
});
// Mock context
mockContext = {
diagnostics: [],
}
})
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("getEffectiveRange", () => {
it("should return selected text when available", () => {
mockDocument.getText.mockReturnValue("selected text")
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');
});
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
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');
});
});
expect(result).toEqual({
range: mockRange,
text: "selected text",
})
})
describe('provideCodeActions', () => {
beforeEach(() => {
mockDocument.getText.mockReturnValue('test code');
mockDocument.lineAt.mockReturnValue({ text: 'test code', lineNumber: 0 });
});
it("should return null for empty line", () => {
mockDocument.getText.mockReturnValue("")
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
it('should provide explain and improve actions by default', () => {
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext);
expect(actions).toHaveLength(2);
expect((actions as any)[0].title).toBe('Roo Cline: Explain Code');
expect((actions as any)[1].title).toBe('Roo Cline: Improve Code');
});
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
it('should provide fix action when diagnostics exist', () => {
mockContext.diagnostics = [{
message: 'test error',
severity: vscode.DiagnosticSeverity.Error,
range: mockRange
}];
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext);
expect(actions).toHaveLength(3);
expect((actions as any).some((a: any) => a.title === 'Roo Cline: Fix Code')).toBe(true);
});
expect(result).toBeNull()
})
})
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 });
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext);
expect(actions).toEqual([]);
expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting effective range:', expect.any(Error));
consoleErrorSpy.mockRestore();
});
});
});
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")
})
})
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", () => {
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toHaveLength(2)
expect((actions as any)[0].title).toBe("Roo Code: Explain Code")
expect((actions as any)[1].title).toBe("Roo Code: Improve Code")
})
it("should provide fix action when diagnostics exist", () => {
mockContext.diagnostics = [
{
message: "test error",
severity: vscode.DiagnosticSeverity.Error,
range: mockRange,
},
]
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toHaveLength(3)
expect((actions as any).some((a: any) => a.title === "Roo Code: Fix Code")).toBe(true)
})
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 })
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toEqual([])
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error))
consoleErrorSpy.mockRestore()
})
})
})

View file

@ -8,8 +8,8 @@ const dotenv = require("dotenv")
const testEnvPath = path.join(__dirname, ".test_env")
dotenv.config({ path: testEnvPath })
suite("Roo Cline Extension Test Suite", () => {
vscode.window.showInformationMessage("Starting Roo Cline extension tests.")
suite("Roo Code Extension Test Suite", () => {
vscode.window.showInformationMessage("Starting Roo Code extension tests.")
test("Extension should be present", () => {
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
@ -117,16 +117,16 @@ suite("Roo Cline Extension Test Suite", () => {
// Test core commands are registered
const expectedCommands = [
'roo-cline.plusButtonClicked',
'roo-cline.mcpButtonClicked',
'roo-cline.historyButtonClicked',
'roo-cline.popoutButtonClicked',
'roo-cline.settingsButtonClicked',
'roo-cline.openInNewTab',
'roo-cline.explainCode',
'roo-cline.fixCode',
'roo-cline.improveCode'
];
"roo-cline.plusButtonClicked",
"roo-cline.mcpButtonClicked",
"roo-cline.historyButtonClicked",
"roo-cline.popoutButtonClicked",
"roo-cline.settingsButtonClicked",
"roo-cline.openInNewTab",
"roo-cline.explainCode",
"roo-cline.fixCode",
"roo-cline.improveCode",
]
for (const cmd of expectedCommands) {
assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`)
@ -136,7 +136,7 @@ suite("Roo Cline Extension Test Suite", () => {
test("Views should be registered", () => {
const view = vscode.window.createWebviewPanel(
"roo-cline.SidebarProvider",
"Roo Cline",
"Roo Code",
vscode.ViewColumn.One,
{},
)
@ -184,17 +184,12 @@ suite("Roo Cline Extension Test Suite", () => {
// Create webview panel with development options
const extensionUri = extension.extensionUri
const panel = vscode.window.createWebviewPanel(
"roo-cline.SidebarProvider",
"Roo Cline",
vscode.ViewColumn.One,
{
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots: [extensionUri],
},
)
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots: [extensionUri],
})
try {
// Initialize webview with development context
@ -299,7 +294,7 @@ suite("Roo Cline Extension Test Suite", () => {
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
)
if (hasResponse) {
console.log('Found response containing "Cline" in provider messages')
console.log('Found response containing "Roo Code" in provider messages')
responseReceived = true
break
}
@ -308,14 +303,14 @@ suite("Roo Cline Extension Test Suite", () => {
// Check provider.cline.clineMessages
const clineMessages = provider.cline?.clineMessages
if (clineMessages && clineMessages.length > 0) {
console.log("Cline messages:", JSON.stringify(clineMessages, null, 2))
console.log("Roo Code messages:", JSON.stringify(clineMessages, null, 2))
// @ts-ignore
const hasResponse = clineMessages.some(
(m: { type: string; text: string }) =>
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
)
if (hasResponse) {
console.log('Found response containing "Cline" in cline messages')
console.log('Found response containing "Roo Code" in cline messages')
responseReceived = true
break
}
@ -327,7 +322,7 @@ suite("Roo Cline Extension Test Suite", () => {
if (!responseReceived) {
console.log("Final provider state:", await provider.getState())
console.log("Final cline messages:", provider.cline?.clineMessages)
throw new Error('Did not receive expected response containing "Cline"')
throw new Error('Did not receive expected response containing "Code"')
}
} finally {
panel.dispose()