Skip to content
Draft
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
6 changes: 6 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { telemetrySettingsSchema } from "./telemetry.js"
import { modeConfigSchema } from "./mode.js"
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
import { languagesSchema } from "./vscode.js"
import { savedPromptSchema } from "./saved-prompt.js"

/**
* Default delay in milliseconds after writes to allow diagnostics to detect potential problems.
Expand Down Expand Up @@ -232,6 +233,11 @@ export const globalSettingsSchema = z.object({
* @default true
*/
showWorktreesInHomeScreen: z.boolean().optional(),

/**
* User-saved prompts that can be quickly inserted into chat
*/
savedPrompts: z.array(savedPromptSchema).optional(),
})

export type GlobalSettings = z.infer<typeof globalSettingsSchema>
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from "./message.js"
export * from "./mode.js"
export * from "./model.js"
export * from "./provider-settings.js"
export * from "./saved-prompt.js"
export * from "./skills.js"
export * from "./task.js"
export * from "./todo.js"
Expand Down
82 changes: 82 additions & 0 deletions packages/types/src/saved-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { z } from "zod"

/**
* SavedPrompt
*
* Represents a user-saved prompt that can be quickly inserted into the chat input.
* Users can optionally associate a prompt with a specific API configuration,
* which will be automatically selected when the prompt is used.
*/
export const savedPromptSchema = z.object({
/**
* Unique identifier for the saved prompt
*/
id: z.string(),

/**
* Display name for the prompt (used in UI and slash commands)
*/
name: z.string(),

/**
* The actual prompt content to be inserted
*/
content: z.string(),

/**
* Optional description for the prompt
*/
description: z.string().optional(),

/**
* Optional API configuration ID to auto-select when using this prompt
*/
apiConfigId: z.string().optional(),

/**
* Timestamp when the prompt was created
*/
createdAt: z.number(),

/**
* Timestamp when the prompt was last updated
*/
updatedAt: z.number(),
})

export type SavedPrompt = z.infer<typeof savedPromptSchema>

/**
* SavedPromptCreate
*
* Payload for creating a new saved prompt (without id and timestamps)
*/
export const savedPromptCreateSchema = savedPromptSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
})

export type SavedPromptCreate = z.infer<typeof savedPromptCreateSchema>

/**
* SavedPromptUpdate
*
* Payload for updating an existing saved prompt
*/
export const savedPromptUpdateSchema = savedPromptSchema.partial().required({ id: true })

export type SavedPromptUpdate = z.infer<typeof savedPromptUpdateSchema>

/**
* SavedPromptsExport
*
* Format for exporting/importing saved prompts
*/
export const savedPromptsExportSchema = z.object({
version: z.literal(1),
exportedAt: z.number(),
prompts: z.array(savedPromptSchema),
})

export type SavedPromptsExport = z.infer<typeof savedPromptsExportSchema>
17 changes: 17 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { SkillMetadata } from "./skills.js"
import type { ModelRecord, RouterModels } from "./model.js"
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
import type { WorktreeIncludeStatus } from "./worktree.js"
import type { SavedPrompt, SavedPromptCreate, SavedPromptUpdate, SavedPromptsExport } from "./saved-prompt.js"

/**
* ExtensionMessage
Expand Down Expand Up @@ -109,6 +110,7 @@ export interface ExtensionMessage {
| "branchWorktreeIncludeResult"
| "folderSelected"
| "skills"
| "savedPrompts"
text?: string
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
checkpointWarning?: {
Expand Down Expand Up @@ -334,6 +336,7 @@ export type ExtensionState = Pick<
| "maxGitStatusFiles"
| "requestDelaySeconds"
| "showWorktreesInHomeScreen"
| "savedPrompts"
> & {
version: string
clineMessages: ClineMessage[]
Expand Down Expand Up @@ -606,6 +609,14 @@ export interface WebviewMessage {
| "deleteSkill"
| "moveSkill"
| "openSkillFile"
// Saved prompts messages
| "requestSavedPrompts"
| "createSavedPrompt"
| "updateSavedPrompt"
| "deleteSavedPrompt"
| "exportSavedPrompts"
| "importSavedPrompts"
| "useSavedPrompt"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down Expand Up @@ -707,6 +718,12 @@ export interface WebviewMessage {
worktreeForce?: boolean
worktreeNewWindow?: boolean
worktreeIncludeContent?: string
// Saved prompts properties
savedPrompt?: SavedPrompt
savedPromptCreate?: SavedPromptCreate
savedPromptUpdate?: SavedPromptUpdate
savedPromptsExport?: SavedPromptsExport
savedPromptId?: string
}

export interface RequestOpenAiCodexRateLimitsMessage {
Expand Down
193 changes: 193 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3167,6 +3167,199 @@ export const webviewMessageHandler = async (
}
break
}

/**
* Saved Prompts
*/

case "requestSavedPrompts": {
const savedPrompts = getGlobalState("savedPrompts") || []
await provider.postMessageToWebview({
type: "savedPrompts",
savedPrompts,
})
break
}

case "createSavedPrompt": {
try {
const promptData = message.savedPromptCreate
if (!promptData) {
provider.log("Missing savedPromptCreate data")
break
}

const savedPrompts = getGlobalState("savedPrompts") || []
const newPrompt = {
...promptData,
id: `prompt-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
createdAt: Date.now(),
updatedAt: Date.now(),
}

await updateGlobalState("savedPrompts", [...savedPrompts, newPrompt])
await provider.postMessageToWebview({
type: "savedPrompts",
savedPrompts: [...savedPrompts, newPrompt],
})
} catch (error) {
provider.log(`Error creating saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
break
}

case "updateSavedPrompt": {
try {
const promptUpdate = message.savedPromptUpdate
if (!promptUpdate?.id) {
provider.log("Missing savedPromptUpdate data or id")
break
}

const savedPrompts = getGlobalState("savedPrompts") || []
const updatedPrompts = savedPrompts.map((prompt: any) =>
prompt.id === promptUpdate.id
? { ...prompt, ...promptUpdate, updatedAt: Date.now() }
: prompt,
)

await updateGlobalState("savedPrompts", updatedPrompts)
await provider.postMessageToWebview({
type: "savedPrompts",
savedPrompts: updatedPrompts,
})
} catch (error) {
provider.log(`Error updating saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
break
}

case "deleteSavedPrompt": {
try {
const promptId = message.savedPromptId
if (!promptId) {
provider.log("Missing savedPromptId")
break
}

const savedPrompts = getGlobalState("savedPrompts") || []
const filteredPrompts = savedPrompts.filter((prompt: any) => prompt.id !== promptId)

await updateGlobalState("savedPrompts", filteredPrompts)
await provider.postMessageToWebview({
type: "savedPrompts",
savedPrompts: filteredPrompts,
})
} catch (error) {
provider.log(`Error deleting saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
break
}

case "exportSavedPrompts": {
try {
const savedPrompts = getGlobalState("savedPrompts") || []
const exportData = {
version: 1 as const,
exportedAt: Date.now(),
prompts: savedPrompts,
}

const defaultUri = await resolveDefaultSaveUri(provider, "savedPrompts", "json", "lastSavedPromptsExportPath")
const saveUri = await vscode.window.showSaveDialog({
defaultUri,
filters: { JSON: ["json"] },
title: t("common:savedPrompts.exportTitle"),
})

if (saveUri) {
await fs.writeFile(saveUri.fsPath, JSON.stringify(exportData, null, 2), "utf8")
vscode.window.showInformationMessage(t("common:savedPrompts.exportSuccess"))
await saveLastExportPath(provider, "lastSavedPromptsExportPath", saveUri.fsPath)
}
} catch (error) {
provider.log(`Error exporting saved prompts: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
vscode.window.showErrorMessage(t("common:savedPrompts.exportError"))
Comment on lines +3272 to +3282
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation keys used here (common:savedPrompts.exportTitle, common:savedPrompts.exportSuccess, common:savedPrompts.exportError, etc.) are not defined in src/i18n/locales/en/common.json. This will cause the i18n system to display raw keys like "common:savedPrompts.exportTitle" instead of user-friendly text in VS Code dialogs. The savedPrompts object needs to be added to the backend's common.json locale file with keys for exportTitle, exportSuccess, exportError, importTitle, invalidFormat, importSuccess, and importError.

Fix it with Roo Code or mention @roomote and request a fix.

}
break
}

case "importSavedPrompts": {
try {
const openUri = await vscode.window.showOpenDialog({
canSelectMany: false,
filters: { JSON: ["json"] },
title: t("common:savedPrompts.importTitle"),
})

if (openUri && openUri[0]) {
const content = await fs.readFile(openUri[0].fsPath, "utf8")
const importData = JSON.parse(content)

// Validate the import data structure
if (!importData.version || !Array.isArray(importData.prompts)) {
vscode.window.showErrorMessage(t("common:savedPrompts.invalidFormat"))
break
}

const existingPrompts = getGlobalState("savedPrompts") || []

// Merge prompts, avoiding duplicates by ID
const existingIds = new Set(existingPrompts.map((p: any) => p.id))
const newPrompts = importData.prompts.filter((p: any) => !existingIds.has(p.id))
const mergedPrompts = [...existingPrompts, ...newPrompts]

await updateGlobalState("savedPrompts", mergedPrompts)
await provider.postMessageToWebview({
type: "savedPrompts",
savedPrompts: mergedPrompts,
})

vscode.window.showInformationMessage(
t("common:savedPrompts.importSuccess", { count: newPrompts.length }),
)
}
} catch (error) {
provider.log(`Error importing saved prompts: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
vscode.window.showErrorMessage(t("common:savedPrompts.importError"))
}
break
}

case "useSavedPrompt": {
try {
const promptId = message.savedPromptId
if (!promptId) {
provider.log("Missing savedPromptId")
break
}

const savedPrompts = getGlobalState("savedPrompts") || []
const prompt = savedPrompts.find((p: any) => p.id === promptId)

if (prompt) {
// If the prompt has an associated API config, switch to it
if (prompt.apiConfigId) {
const listApiConfigMeta = getGlobalState("listApiConfigMeta") || []
const targetConfig = listApiConfigMeta.find((config: any) => config.id === prompt.apiConfigId)
if (targetConfig) {
await updateGlobalState("currentApiConfigName", targetConfig.name)
await provider.postStateToWebview()
}
}

// Insert the prompt content into the textarea (replacing current content)
await provider.postMessageToWebview({
type: "insertTextIntoTextarea",
text: prompt.content,
})
}
} catch (error) {
provider.log(`Error using saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
break
}

case "showMdmAuthRequiredNotification": {
// Show notification that organization requires authentication
vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
Expand Down
Loading
Loading