Skip to content

Commit ca6f151

Browse files
committed
feat(custom-instructions): migrate custom instructions to file-based storage
See discussion: #4000 Migrated the storage of "Custom Instructions for All Modes" from VS Code's global state to a dedicated file: `...Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/custom_instructions.md`. Key changes include: - Implemented a migration utility to automatically move existing custom instructions from global state to the new file upon extension update. - Introduced new commands and UI buttons within the Prompts view to: - Open the `custom_instructions.md` file directly in the editor for easy editing. - Refresh the custom instructions in the UI by re-reading them from the file, ensuring changes made directly to the file are reflected. - The `updateCustomInstructions` function now writes/deletes the file instead of updating global state. - The initial state and refreshes now read custom instructions directly from the file. This change provides a more robust and user-friendly way to manage custom instructions, allowing for external editing and version control.
1 parent 6a232a9 commit ca6f151

File tree

7 files changed

+109
-5
lines changed

7 files changed

+109
-5
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -989,11 +989,47 @@ export class ClineProvider
989989
}
990990

991991
async updateCustomInstructions(instructions?: string) {
992-
// User may be clearing the field.
993-
await this.updateGlobalState("customInstructions", instructions || undefined)
992+
const settingsDirPath = await this.ensureSettingsDirectoryExists()
993+
const customInstructionsFilePath = path.join(settingsDirPath, GlobalFileNames.customInstructions)
994+
995+
if (instructions && instructions.trim()) {
996+
await fs.writeFile(customInstructionsFilePath, instructions.trim(), "utf-8")
997+
} else {
998+
// If instructions are empty or undefined, delete the file if it exists
999+
try {
1000+
await fs.unlink(customInstructionsFilePath)
1001+
} catch (error) {
1002+
// Ignore if file doesn't exist
1003+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1004+
throw error
1005+
}
1006+
}
1007+
}
9941008
await this.postStateToWebview()
9951009
}
9961010

1011+
async openCustomInstructionsFile(): Promise<void> {
1012+
const settingsDirPath = await this.ensureSettingsDirectoryExists()
1013+
const customInstructionsFilePath = path.join(settingsDirPath, GlobalFileNames.customInstructions)
1014+
const fileUri = vscode.Uri.file(customInstructionsFilePath)
1015+
await vscode.commands.executeCommand("vscode.open", fileUri, { preview: false, preserveFocus: true })
1016+
await vscode.commands.executeCommand("workbench.action.files.revert", fileUri)
1017+
}
1018+
1019+
async refreshCustomInstructions(): Promise<void> {
1020+
const settingsDirPath = await this.ensureSettingsDirectoryExists()
1021+
const customInstructionsFilePath = path.join(settingsDirPath, GlobalFileNames.customInstructions)
1022+
let content: string | undefined = undefined
1023+
try {
1024+
content = await fs.readFile(customInstructionsFilePath, "utf-8")
1025+
} catch (error) {
1026+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1027+
throw error
1028+
}
1029+
}
1030+
await this.updateCustomInstructions(content)
1031+
}
1032+
9971033
// MCP
9981034

9991035
async ensureMcpServersDirectoryExists(): Promise<string> {
@@ -1025,6 +1061,21 @@ export class ClineProvider
10251061
return getSettingsDirectoryPath(globalStoragePath)
10261062
}
10271063

1064+
private async readCustomInstructionsFromFile(): Promise<string | undefined> {
1065+
const settingsDirPath = await this.ensureSettingsDirectoryExists()
1066+
const customInstructionsFilePath = path.join(settingsDirPath, GlobalFileNames.customInstructions)
1067+
1068+
if (await fileExistsAtPath(customInstructionsFilePath)) {
1069+
try {
1070+
return await fs.readFile(customInstructionsFilePath, "utf-8")
1071+
} catch (error) {
1072+
this.log(`Error reading custom instructions file: ${error}`)
1073+
return undefined
1074+
}
1075+
}
1076+
return undefined
1077+
}
1078+
10281079
// OpenRouter
10291080

10301081
async handleOpenRouterCallback(code: string) {
@@ -1474,7 +1525,7 @@ export class ClineProvider
14741525
return {
14751526
apiConfiguration: providerSettings,
14761527
lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
1477-
customInstructions: stateValues.customInstructions,
1528+
customInstructions: await this.readCustomInstructionsFromFile(),
14781529
apiModelId: stateValues.apiModelId,
14791530
alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
14801531
alwaysAllowReadOnlyOutsideWorkspace: stateValues.alwaysAllowReadOnlyOutsideWorkspace ?? false,

src/core/webview/webviewMessageHandler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { supportPrompt } from "../../shared/support-prompt"
1616
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
1717
import { checkExistKey } from "../../shared/checkExistApiConfig"
1818
import { experimentDefault } from "../../shared/experiments"
19+
import { GlobalFileNames } from "../../shared/globalFileNames"
1920
import { Terminal } from "../../integrations/terminal/Terminal"
2021
import { openFile, openImage } from "../../integrations/misc/open-file"
2122
import { selectImages } from "../../integrations/misc/process-images"
@@ -132,6 +133,12 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
132133
case "customInstructions":
133134
await provider.updateCustomInstructions(message.text)
134135
break
136+
case "openCustomInstructionsFile":
137+
await provider.openCustomInstructionsFile()
138+
break
139+
case "refreshCustomInstructions":
140+
await provider.refreshCustomInstructions()
141+
break
135142
case "alwaysAllowReadOnly":
136143
await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
137144
await provider.postStateToWebview()

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ export interface WebviewMessage {
118118
| "deleteCustomMode"
119119
| "setopenAiCustomModelInfo"
120120
| "openCustomModesSettings"
121+
| "openCustomInstructionsFile"
122+
| "refreshCustomInstructions"
121123
| "checkpointDiff"
122124
| "checkpointRestore"
123125
| "deleteMcpServer"

src/shared/globalFileNames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export const GlobalFileNames = {
33
uiMessages: "ui_messages.json",
44
mcpSettings: "mcp_settings.json",
55
customModes: "custom_modes.yaml",
6+
customInstructions: "custom_instructions.md",
67
taskMetadata: "task_metadata.json",
78
}

src/utils/migrateSettings.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ export async function migrateSettings(
3434

3535
// Process each file migration
3636
try {
37+
// Migrate custom instructions from GlobalState to file
38+
const customInstructionsKey = "customInstructions"
39+
const customInstructionsContent = context.globalState.get<string>(customInstructionsKey)
40+
const customInstructionsFilePath = path.join(settingsDir, GlobalFileNames.customInstructions)
41+
42+
if (customInstructionsContent && !(await fileExistsAtPath(customInstructionsFilePath))) {
43+
await fs.writeFile(customInstructionsFilePath, customInstructionsContent, "utf-8")
44+
await context.globalState.update(customInstructionsKey, undefined) // Delete from GlobalState
45+
outputChannel.appendLine("Migrated custom instructions from GlobalState to file.")
46+
} else {
47+
outputChannel.appendLine(
48+
`Skipping custom instructions migration: ${customInstructionsContent ? "file already exists" : "no data in GlobalState"}`,
49+
)
50+
}
51+
3752
for (const migration of fileMigrations) {
3853
const oldPath = path.join(settingsDir, migration.oldName)
3954
const newPath = path.join(settingsDir, migration.newName)

webview-ui/src/components/modes/ModesView.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,8 +1036,34 @@ const ModesView = ({ onDone }: ModesViewProps) => {
10361036
</div>
10371037
</div>
10381038

1039-
<div className="pb-5">
1040-
<h3 className="text-vscode-foreground mb-3">{t("prompts:globalCustomInstructions.title")}</h3>
1039+
<div className="pb-5 border-b border-vscode-input-border">
1040+
<div className="flex justify-between items-center mb-3">
1041+
<h3 className="text-vscode-foreground m-0">{t("prompts:globalCustomInstructions.title")}</h3>
1042+
<div className="flex gap-2">
1043+
<Button
1044+
variant="ghost"
1045+
size="icon"
1046+
onClick={() => {
1047+
vscode.postMessage({
1048+
type: "openCustomInstructionsFile",
1049+
})
1050+
}}
1051+
title={t("prompts:globalCustomInstructions.openFile")}>
1052+
<span className="codicon codicon-go-to-file"></span>
1053+
</Button>
1054+
<Button
1055+
variant="ghost"
1056+
size="icon"
1057+
onClick={() => {
1058+
vscode.postMessage({
1059+
type: "refreshCustomInstructions",
1060+
})
1061+
}}
1062+
title={t("prompts:globalCustomInstructions.refreshFile")}>
1063+
<span className="codicon codicon-refresh"></span>
1064+
</Button>
1065+
</div>
1066+
</div>
10411067

10421068
<div className="text-sm text-vscode-descriptionForeground mb-2">
10431069
<Trans i18nKey="prompts:globalCustomInstructions.description">

webview-ui/src/i18n/locales/en/prompts.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"globalCustomInstructions": {
4949
"title": "Custom Instructions for All Modes",
5050
"description": "These instructions apply to all modes. They provide a base set of behaviors that can be enhanced by mode-specific instructions below. <0>Learn more</0>",
51+
"openFile": "Open custom instructions file",
52+
"refreshFile": "Refresh custom instructions from file",
5153
"loadFromFile": "Instructions can also be loaded from the <span>.roo/rules/</span> folder in your workspace (.roorules and .clinerules are deprecated and will stop working soon)."
5254
},
5355
"systemPrompt": {

0 commit comments

Comments
 (0)