Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed bin/roo-cline-1.0.1.vsix
Binary file not shown.
Binary file added bin/roo-cline-1.0.2.vsix
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "roo-cline",
"displayName": "Roo Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "1.0.1",
"version": "1.0.2",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
Expand Down
6 changes: 1 addition & 5 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,11 +757,7 @@ export class Cline {
}

async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
if (this.customInstructions && this.customInstructions.trim()) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addCustomInstructions(this.customInstructions)
}
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false) + await addCustomInstructions(this.customInstructions ?? '', cwd)

// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
Expand Down
112 changes: 112 additions & 0 deletions src/core/prompts/__tests__/system.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
import { addCustomInstructions } from '../system'

// Mock external dependencies
jest.mock('os-name', () => () => 'macOS')
jest.mock('default-shell', () => '/bin/zsh')
jest.mock('os', () => ({
homedir: () => '/Users/test',
...jest.requireActual('os')
}))

describe('system.ts', () => {
let tempDir: string

beforeEach(async () => {
// Create a temporary directory for test files
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cline-test-'))
})

afterEach(async () => {
// Clean up temporary directory after each test
await fs.rm(tempDir, { recursive: true, force: true })
})

describe('addCustomInstructions', () => {
it('should include content from .clinerules and .cursorrules if present', async () => {
// Create test rule files
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests\nUse TypeScript')
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')

const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)

// Verify all instructions are included
expect(result).toContain('Base instructions')
expect(result).toContain('Always write tests')
expect(result).toContain('Use TypeScript')
expect(result).toContain('Format code before committing')
expect(result).toContain('Rules from .clinerules:')
expect(result).toContain('Rules from .cursorrules:')
})

it('should handle missing rule files gracefully', async () => {
const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)

// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})

it('should handle empty rule files', async () => {
// Create empty rule files
await fs.writeFile(path.join(tempDir, '.clinerules'), '')
await fs.writeFile(path.join(tempDir, '.cursorrules'), '')

const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)

// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})

it('should handle whitespace-only rule files', async () => {
// Create rule files with only whitespace
await fs.writeFile(path.join(tempDir, '.clinerules'), ' \n \t ')
await fs.writeFile(path.join(tempDir, '.cursorrules'), ' \n ')

const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)

// Should only contain base instructions
expect(result).toContain('Base instructions')
expect(result).not.toContain('Rules from')
})

it('should handle one rule file present and one missing', async () => {
// Create only .clinerules
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')

const customInstructions = 'Base instructions'
const result = await addCustomInstructions(customInstructions, tempDir)

// Should contain base instructions and .clinerules content
expect(result).toContain('Base instructions')
expect(result).toContain('Always write tests')
expect(result).toContain('Rules from .clinerules:')
expect(result).not.toContain('Rules from .cursorrules:')
})

it('should handle empty custom instructions with rule files', async () => {
await fs.writeFile(path.join(tempDir, '.clinerules'), 'Always write tests')
await fs.writeFile(path.join(tempDir, '.cursorrules'), 'Format code before committing')

const result = await addCustomInstructions('', tempDir)

// Should contain rule file content even with empty custom instructions
expect(result).toContain('Always write tests')
expect(result).toContain('Format code before committing')
expect(result).toContain('Rules from .clinerules:')
expect(result).toContain('Rules from .cursorrules:')
})

it('should return empty string when no instructions or rules exist', async () => {
const result = await addCustomInstructions('', tempDir)
expect(result).toBe('')
})
})
})
39 changes: 36 additions & 3 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import osName from "os-name"
import defaultShell from "default-shell"
import os from "os"
import fs from 'fs/promises'
import path from 'path'

export const SYSTEM_PROMPT = async (
cwd: string,
Expand Down Expand Up @@ -281,13 +283,44 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built.
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`

export function addCustomInstructions(customInstructions: string): string {
return `
async function loadRuleFiles(cwd: string): Promise<string> {
const ruleFiles = ['.clinerules', '.cursorrules']
let combinedRules = ''

for (const file of ruleFiles) {
try {
const content = await fs.readFile(path.join(cwd, file), 'utf-8')
if (content.trim()) {
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
}
} catch (err) {
// Silently skip if file doesn't exist
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err
}
}
}

return combinedRules
}

export async function addCustomInstructions(customInstructions: string, cwd: string): Promise<string> {
const ruleFileContent = await loadRuleFiles(cwd)
const allInstructions = [customInstructions.trim()]

if (ruleFileContent && ruleFileContent.trim()) {
allInstructions.push(ruleFileContent.trim())
}

const joinedInstructions = allInstructions.join('\n\n')

return joinedInstructions ? `
====

USER'S CUSTOM INSTRUCTIONS

The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.

${customInstructions.trim()}`
${joinedInstructions}`
: ""
}