-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(custom-instructions): migrate custom instructions to file-based storage #4002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ca6f151
7aab01d
86d8d05
1d8e86a
bf6cc42
4f233b9
d5f522f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { RouterName, toRouterName, ModelRecord } from "../../shared/api" | |
| import { supportPrompt } from "../../shared/support-prompt" | ||
|
|
||
| import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" | ||
| import { GlobalContentIds } from "../../shared/globalContentIds" | ||
| import { checkExistKey } from "../../shared/checkExistApiConfig" | ||
| import { experimentDefault } from "../../shared/experiments" | ||
| import { Terminal } from "../../integrations/terminal/Terminal" | ||
|
|
@@ -130,7 +131,23 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We | |
| await provider.initClineWithTask(message.text, message.images) | ||
| break | ||
| case "customInstructions": | ||
| await provider.updateCustomInstructions(message.text) | ||
| await provider.contentManager.updateContent(GlobalContentIds.customInstructions, message.text) | ||
| break | ||
| case "openContent": | ||
| if (message.contentId) { | ||
| // Check for contentId instead of filePath | ||
| await provider.contentManager.openContent(message.contentId) | ||
| } else { | ||
| console.error("openContent message missing contentId") | ||
|
||
| } | ||
| break | ||
| case "refreshContent": | ||
| if (message.contentId) { | ||
| await provider.contentManager.refreshContent(message.contentId) | ||
| } else { | ||
| // Handle error or log if contentId is missing | ||
| console.error("refreshContent message missing contentId") | ||
| } | ||
| break | ||
| case "alwaysAllowReadOnly": | ||
| await updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import * as vscode from "vscode" | ||
| import * as path from "path" | ||
| import fs from "fs/promises" | ||
|
|
||
| import { GlobalContentIds } from "../../shared/globalContentIds" | ||
| import { GlobalFileNames } from "../../shared/globalFileNames" | ||
| import { openFile } from "../../integrations/misc/open-file" | ||
| import { safeReadFile } from "../../utils/fs" | ||
| import { getSettingsDirectoryPath } from "../../utils/storage" // Need to confirm this import path | ||
| import { ExtensionMessage } from "../../shared/ExtensionMessage" // Assuming this type is needed for postMessageToWebview | ||
|
|
||
| // Define an interface for the dependencies ContentManager needs from ClineProvider | ||
| interface ContentManagerDependencies { | ||
| log: (message: string) => void | ||
| postMessageToWebview: (message: ExtensionMessage) => Promise<void> | ||
| postStateToWebview: () => Promise<void> | ||
| globalStorageUriFsPath: string // To replace contextProxy.globalStorageUri.fsPath | ||
| } | ||
|
|
||
| export class ContentManager { | ||
| private readonly log: (message: string) => void | ||
| private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void> | ||
| private readonly postStateToWebview: () => Promise<void> | ||
| private readonly globalStorageUriFsPath: string | ||
|
|
||
| constructor(dependencies: ContentManagerDependencies) { | ||
| this.log = dependencies.log | ||
| this.postMessageToWebview = dependencies.postMessageToWebview | ||
| this.postStateToWebview = dependencies.postStateToWebview | ||
| this.globalStorageUriFsPath = dependencies.globalStorageUriFsPath | ||
| } | ||
|
|
||
| private async ensureSettingsDirectoryExists(): Promise<string> { | ||
| const globalStoragePath = this.globalStorageUriFsPath | ||
| return getSettingsDirectoryPath(globalStoragePath) | ||
| } | ||
|
|
||
| private async getContentPath(contentId: string): Promise<string | undefined> { | ||
| const settingsDir = await this.ensureSettingsDirectoryExists() | ||
| switch (contentId) { | ||
| case GlobalContentIds.customInstructions: | ||
| return path.join(settingsDir, GlobalFileNames.customInstructions) | ||
| default: | ||
| this.log(`Unknown contentId: ${contentId}`) | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| async openContent(contentId: string): Promise<void> { | ||
| const filePath = await this.getContentPath(contentId) | ||
|
|
||
| if (!filePath) { | ||
| this.log(`File path could not be determined for contentId: ${contentId}`) | ||
| return | ||
| } | ||
|
|
||
| await openFile(filePath, { create: true }) | ||
| await vscode.commands.executeCommand("workbench.action.files.revert") | ||
| } | ||
|
|
||
| async refreshContent(contentId: string): Promise<void> { | ||
| const content = await this.readContent(contentId) | ||
| await this.updateContent(contentId, content) | ||
| this.postMessageToWebview({ | ||
| type: "contentRefreshed", | ||
| contentId: contentId, | ||
| success: true, // Assuming success for now | ||
| }) | ||
| } | ||
|
|
||
| async updateContent(contentId: string, content?: string) { | ||
| const filePath = await this.getContentPath(contentId) | ||
|
|
||
| if (!filePath) { | ||
| this.log(`File path could not be determined for contentId: ${contentId}`) | ||
| return | ||
| } | ||
|
|
||
| if (content && content.trim()) { | ||
| await fs.writeFile(filePath, content.trim(), "utf-8") | ||
| this.log(`Updated content file: ${filePath}`) | ||
| } else { | ||
| try { | ||
| await fs.unlink(filePath) | ||
| this.log(`Deleted content file: ${filePath}`) | ||
| } catch (error: any) { | ||
| if (error.code !== "ENOENT") { | ||
| this.log(`Error deleting content file: ${error.message}`) | ||
| throw error | ||
| } | ||
| } | ||
| } | ||
| // Update the webview state | ||
| await this.postStateToWebview() | ||
| } | ||
|
|
||
| async readContent(contentId: string): Promise<string> { | ||
| const filePath = await this.getContentPath(contentId) | ||
|
|
||
| return filePath ? ((await safeReadFile(filePath)) ?? "") : "" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const GlobalContentIds = { | ||
| customInstructions: "customInstructions" as const, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,3 +45,19 @@ export async function fileExistsAtPath(filePath: string): Promise<boolean> { | |
| return false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Safely read a file and return its trimmed content | ||
| */ | ||
| export async function safeReadFile(filePath: string): Promise<string> { | ||
| try { | ||
| const content = await fs.readFile(filePath, "utf-8") | ||
| return content ? content.trim() : "" | ||
| } catch (err) { | ||
| const errorCode = (err as NodeJS.ErrnoException).code | ||
| if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) { | ||
|
||
| throw err | ||
| } | ||
| return "" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason to have these methods inside
ClineProvider?If not, it might be a good idea to move them out since this class is already quite big.