Skip to content

Commit 9686762

Browse files
committed
fix: enhance addCustomInstructions to prioritize mode-specific instructions and prevent duplicates
1 parent f6cae4d commit 9686762

File tree

3 files changed

+169
-6
lines changed

3 files changed

+169
-6
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
# npm install --global @vscode/vsce
3+
# install:
4+
# code --install-extension /workspaces/Roo-CodeDev/roo-cline-testversion-XXX.vsix
5+
6+
# Define original and test names
7+
ORIGINAL_NAME="roo-cline"
8+
TEST_NAME="roo-cline-testversion" # VSIX names typically don't have spaces or special chars
9+
10+
ORIGINAL_DISPLAY_NAME="%extension.displayName%"
11+
TEST_DISPLAY_NAME="Roo Code (Testversion)"
12+
13+
# Backup package.json
14+
cp package.json package.json.bak
15+
16+
echo "Modifying package.json for test build..."
17+
# Replace the name in package.json
18+
sed -i 's/"name": "'"$ORIGINAL_NAME"'"/"name": "'"$TEST_NAME"'"/' package.json
19+
# Replace the displayName in package.json
20+
# Note: Using a different delimiter for sed because the replacement string contains '%'
21+
sed -i 's#"displayName": "'"$ORIGINAL_DISPLAY_NAME"'"#"displayName": "'"$TEST_DISPLAY_NAME"'"#' package.json
22+
23+
echo "Packaging the extension..."
24+
# Package the extension with vsce
25+
vsce package
26+
27+
echo "Reverting package.json..."
28+
# Revert the name change after packaging
29+
mv package.json.bak package.json
30+
31+
echo "Done. Test version packaged."
32+
echo "The VSIX file will be named something like: ${TEST_NAME}-<version>.vsix"

src/core/prompts/sections/__tests__/custom-instructions.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ describe("loadRuleFiles", () => {
298298
describe("addCustomInstructions", () => {
299299
beforeEach(() => {
300300
jest.clearAllMocks()
301+
// Default mocks for rule loading to prevent errors in tests focused on custom instructions
302+
statMock.mockRejectedValue({ code: "ENOENT" }) // Simulate no .roo/rules-mode or .roo/rules dir
303+
readFileMock.mockRejectedValue({ code: "ENOENT" }) // Simulate no rule files
301304
})
302305

303306
it("should combine all instruction types when provided", async () => {
@@ -566,6 +569,130 @@ describe("addCustomInstructions", () => {
566569

567570
expect(statCallCount).toBeGreaterThan(0)
568571
})
572+
573+
it("should prioritize mode-specific instructions and only add different global instructions", async () => {
574+
const modeInstructions = "Mode-specific content"
575+
const globalInstructions = "Global content"
576+
// Simulate no rule files for simplicity in this test
577+
statMock.mockRejectedValue({ code: "ENOENT" })
578+
readFileMock.mockRejectedValue({ code: "ENOENT" })
579+
580+
const result = await addCustomInstructions(modeInstructions, globalInstructions, "/fake/path", "test-mode", {})
581+
expect(result).toContain(`Mode-specific Instructions:\n${modeInstructions}`)
582+
expect(result).toContain(`Global Instructions:\n${globalInstructions}`)
583+
// Ensure Mode-specific appears before Global
584+
expect(result.indexOf(`Mode-specific Instructions:\n${modeInstructions}`)).toBeLessThan(
585+
result.indexOf(`Global Instructions:\n${globalInstructions}`),
586+
)
587+
})
588+
589+
it("should only add mode-specific instructions if global instructions are identical", async () => {
590+
const sharedInstructions = "Shared content"
591+
// Simulate no rule files
592+
statMock.mockRejectedValue({ code: "ENOENT" })
593+
readFileMock.mockRejectedValue({ code: "ENOENT" })
594+
595+
const result = await addCustomInstructions(
596+
sharedInstructions,
597+
sharedInstructions,
598+
"/fake/path",
599+
"test-mode",
600+
{},
601+
)
602+
expect(result).toContain(`Mode-specific Instructions:\n${sharedInstructions}`)
603+
expect(result).not.toContain("Global Instructions:")
604+
})
605+
606+
it("should only add global instructions if mode-specific instructions are empty", async () => {
607+
const globalInstructions = "Global content only"
608+
// Simulate no rule files
609+
statMock.mockRejectedValue({ code: "ENOENT" })
610+
readFileMock.mockRejectedValue({ code: "ENOENT" })
611+
612+
const result = await addCustomInstructions("", globalInstructions, "/fake/path", "test-mode", {})
613+
expect(result).not.toContain("Mode-specific Instructions:")
614+
expect(result).toContain(`Global Instructions:\n${globalInstructions}`)
615+
})
616+
617+
it("should only add mode-specific instructions if global instructions are empty", async () => {
618+
const modeInstructions = "Mode-specific content only"
619+
// Simulate no rule files
620+
statMock.mockRejectedValue({ code: "ENOENT" })
621+
readFileMock.mockRejectedValue({ code: "ENOENT" })
622+
623+
const result = await addCustomInstructions(modeInstructions, "", "/fake/path", "test-mode", {})
624+
expect(result).toContain(`Mode-specific Instructions:\n${modeInstructions}`)
625+
expect(result).not.toContain("Global Instructions:")
626+
})
627+
628+
it("should handle both instructions being empty strings", async () => {
629+
// Simulate no rule files
630+
statMock.mockRejectedValue({ code: "ENOENT" })
631+
readFileMock.mockRejectedValue({ code: "ENOENT" })
632+
633+
const result = await addCustomInstructions("", "", "/fake/path", "test-mode", {})
634+
expect(result).not.toContain("Mode-specific Instructions:")
635+
expect(result).not.toContain("Global Instructions:")
636+
// If rules are also empty, the result should be an empty string (as per existing test "should return empty string when no instructions provided")
637+
// If there were rules, it would contain the rules section.
638+
})
639+
640+
it("should handle mode-specific instructions with whitespace and global instructions with content", async () => {
641+
const globalInstructions = "Global content"
642+
// Simulate no rule files
643+
statMock.mockRejectedValue({ code: "ENOENT" })
644+
readFileMock.mockRejectedValue({ code: "ENOENT" })
645+
646+
const result = await addCustomInstructions(" ", globalInstructions, "/fake/path", "test-mode", {})
647+
expect(result).not.toContain("Mode-specific Instructions:")
648+
expect(result).toContain(`Global Instructions:\n${globalInstructions}`)
649+
})
650+
651+
it("should handle global instructions with whitespace and mode-specific instructions with content", async () => {
652+
const modeInstructions = "Mode-specific content"
653+
// Simulate no rule files
654+
statMock.mockRejectedValue({ code: "ENOENT" })
655+
readFileMock.mockRejectedValue({ code: "ENOENT" })
656+
657+
const result = await addCustomInstructions(modeInstructions, " ", "/fake/path", "test-mode", {})
658+
expect(result).toContain(`Mode-specific Instructions:\n${modeInstructions}`)
659+
expect(result).not.toContain("Global Instructions:")
660+
})
661+
662+
it("should correctly handle the scenario described by the user (duplicate content from different sources)", async () => {
663+
const userGlobalInstructions = 'start text "Custom Instructions for All Modes" text end'
664+
const modeDefaultInstructions =
665+
"You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer."
666+
667+
// Simulate no rule files
668+
statMock.mockRejectedValue({ code: "ENOENT" })
669+
readFileMock.mockRejectedValue({ code: "ENOENT" })
670+
671+
// Scenario 1: Mode instructions are the default, global are different (user-entered)
672+
let result = await addCustomInstructions(
673+
modeDefaultInstructions,
674+
userGlobalInstructions,
675+
"/fake/path",
676+
"ask-mode",
677+
{},
678+
)
679+
expect(result).toContain(`Mode-specific Instructions:\n${modeDefaultInstructions}`)
680+
expect(result).toContain(`Global Instructions:\n${userGlobalInstructions}`)
681+
expect(result.indexOf(`Mode-specific Instructions:\n${modeDefaultInstructions}`)).toBeLessThan(
682+
result.indexOf(`Global Instructions:\n${userGlobalInstructions}`),
683+
)
684+
685+
// Scenario 2: Mode instructions and global instructions are identical (e.g. user copies mode default into global)
686+
result = await addCustomInstructions(
687+
modeDefaultInstructions,
688+
modeDefaultInstructions,
689+
"/fake/path",
690+
"ask-mode",
691+
{},
692+
)
693+
expect(result).toContain(`Mode-specific Instructions:\n${modeDefaultInstructions}`)
694+
expect(result).not.toContain("Global Instructions:\n") // Note: checking for "Global Instructions:\n" to ensure the section header isn't there
695+
})
569696
})
570697

571698
// Test directory existence checks through loadRuleFiles

src/core/prompts/sections/custom-instructions.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,20 @@ export async function addCustomInstructions(
226226
)
227227
}
228228

229-
// Add global instructions first
230-
if (typeof globalCustomInstructions === "string" && globalCustomInstructions.trim()) {
231-
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
232-
}
233-
234-
// Add mode-specific instructions after
229+
// Add mode-specific instructions first (higher priority)
235230
if (typeof modeCustomInstructions === "string" && modeCustomInstructions.trim()) {
236231
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`)
237232
}
238233

234+
// Only add global instructions if they differ from mode-specific instructions
235+
if (
236+
typeof globalCustomInstructions === "string" &&
237+
globalCustomInstructions.trim() &&
238+
(!modeCustomInstructions || modeCustomInstructions.trim() !== globalCustomInstructions.trim())
239+
) {
240+
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
241+
}
242+
239243
// Add rules - include both mode-specific and generic rules if they exist
240244
const rules = []
241245

0 commit comments

Comments
 (0)