Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 0be1980

Browse files
committed
feat: add saved prompts feature
Implements the Saved Prompts feature as requested in issue #11151. Features: - Create, edit, and delete saved prompts - Associate prompts with specific API configurations - Auto-switch API config when using a prompt - Import/export saved prompts as JSON - Bookmark icon dropdown near chat input for quick access - Full CRUD management in Settings Files added/modified: - packages/types/src/saved-prompt.ts: Zod schema and types - packages/types/src/global-settings.ts: Add savedPrompts to schema - packages/types/src/vscode-extension-host.ts: Message types - src/core/webview/webviewMessageHandler.ts: CRUD handlers - webview-ui/src/components/settings/SavedPromptsSettings.tsx: Settings UI - webview-ui/src/components/chat/SavedPromptsDropdown.tsx: Chat dropdown - webview-ui/src/context/ExtensionStateContext.tsx: State management - webview-ui/src/i18n/locales/en/settings.json: Translations - webview-ui/src/i18n/locales/en/chat.json: Translations Closes #11151
1 parent ede1d29 commit 0be1980

10 files changed

Lines changed: 798 additions & 0 deletions

File tree

packages/types/src/global-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { telemetrySettingsSchema } from "./telemetry.js"
1414
import { modeConfigSchema } from "./mode.js"
1515
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
1616
import { languagesSchema } from "./vscode.js"
17+
import { savedPromptSchema } from "./saved-prompt.js"
1718

1819
/**
1920
* Default delay in milliseconds after writes to allow diagnostics to detect potential problems.
@@ -232,6 +233,11 @@ export const globalSettingsSchema = z.object({
232233
* @default true
233234
*/
234235
showWorktreesInHomeScreen: z.boolean().optional(),
236+
237+
/**
238+
* User-saved prompts that can be quickly inserted into chat
239+
*/
240+
savedPrompts: z.array(savedPromptSchema).optional(),
235241
})
236242

237243
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from "./message.js"
1919
export * from "./mode.js"
2020
export * from "./model.js"
2121
export * from "./provider-settings.js"
22+
export * from "./saved-prompt.js"
2223
export * from "./skills.js"
2324
export * from "./task.js"
2425
export * from "./todo.js"

packages/types/src/saved-prompt.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { z } from "zod"
2+
3+
/**
4+
* SavedPrompt
5+
*
6+
* Represents a user-saved prompt that can be quickly inserted into the chat input.
7+
* Users can optionally associate a prompt with a specific API configuration,
8+
* which will be automatically selected when the prompt is used.
9+
*/
10+
export const savedPromptSchema = z.object({
11+
/**
12+
* Unique identifier for the saved prompt
13+
*/
14+
id: z.string(),
15+
16+
/**
17+
* Display name for the prompt (used in UI and slash commands)
18+
*/
19+
name: z.string(),
20+
21+
/**
22+
* The actual prompt content to be inserted
23+
*/
24+
content: z.string(),
25+
26+
/**
27+
* Optional description for the prompt
28+
*/
29+
description: z.string().optional(),
30+
31+
/**
32+
* Optional API configuration ID to auto-select when using this prompt
33+
*/
34+
apiConfigId: z.string().optional(),
35+
36+
/**
37+
* Timestamp when the prompt was created
38+
*/
39+
createdAt: z.number(),
40+
41+
/**
42+
* Timestamp when the prompt was last updated
43+
*/
44+
updatedAt: z.number(),
45+
})
46+
47+
export type SavedPrompt = z.infer<typeof savedPromptSchema>
48+
49+
/**
50+
* SavedPromptCreate
51+
*
52+
* Payload for creating a new saved prompt (without id and timestamps)
53+
*/
54+
export const savedPromptCreateSchema = savedPromptSchema.omit({
55+
id: true,
56+
createdAt: true,
57+
updatedAt: true,
58+
})
59+
60+
export type SavedPromptCreate = z.infer<typeof savedPromptCreateSchema>
61+
62+
/**
63+
* SavedPromptUpdate
64+
*
65+
* Payload for updating an existing saved prompt
66+
*/
67+
export const savedPromptUpdateSchema = savedPromptSchema.partial().required({ id: true })
68+
69+
export type SavedPromptUpdate = z.infer<typeof savedPromptUpdateSchema>
70+
71+
/**
72+
* SavedPromptsExport
73+
*
74+
* Format for exporting/importing saved prompts
75+
*/
76+
export const savedPromptsExportSchema = z.object({
77+
version: z.literal(1),
78+
exportedAt: z.number(),
79+
prompts: z.array(savedPromptSchema),
80+
})
81+
82+
export type SavedPromptsExport = z.infer<typeof savedPromptsExportSchema>

packages/types/src/vscode-extension-host.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { SkillMetadata } from "./skills.js"
2222
import type { ModelRecord, RouterModels } from "./model.js"
2323
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
2424
import type { WorktreeIncludeStatus } from "./worktree.js"
25+
import type { SavedPrompt, SavedPromptCreate, SavedPromptUpdate, SavedPromptsExport } from "./saved-prompt.js"
2526

2627
/**
2728
* ExtensionMessage
@@ -109,6 +110,7 @@ export interface ExtensionMessage {
109110
| "branchWorktreeIncludeResult"
110111
| "folderSelected"
111112
| "skills"
113+
| "savedPrompts"
112114
text?: string
113115
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
114116
checkpointWarning?: {
@@ -334,6 +336,7 @@ export type ExtensionState = Pick<
334336
| "maxGitStatusFiles"
335337
| "requestDelaySeconds"
336338
| "showWorktreesInHomeScreen"
339+
| "savedPrompts"
337340
> & {
338341
version: string
339342
clineMessages: ClineMessage[]
@@ -606,6 +609,14 @@ export interface WebviewMessage {
606609
| "deleteSkill"
607610
| "moveSkill"
608611
| "openSkillFile"
612+
// Saved prompts messages
613+
| "requestSavedPrompts"
614+
| "createSavedPrompt"
615+
| "updateSavedPrompt"
616+
| "deleteSavedPrompt"
617+
| "exportSavedPrompts"
618+
| "importSavedPrompts"
619+
| "useSavedPrompt"
609620
text?: string
610621
editedMessageContent?: string
611622
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -707,6 +718,12 @@ export interface WebviewMessage {
707718
worktreeForce?: boolean
708719
worktreeNewWindow?: boolean
709720
worktreeIncludeContent?: string
721+
// Saved prompts properties
722+
savedPrompt?: SavedPrompt
723+
savedPromptCreate?: SavedPromptCreate
724+
savedPromptUpdate?: SavedPromptUpdate
725+
savedPromptsExport?: SavedPromptsExport
726+
savedPromptId?: string
710727
}
711728

712729
export interface RequestOpenAiCodexRateLimitsMessage {

src/core/webview/webviewMessageHandler.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3167,6 +3167,199 @@ export const webviewMessageHandler = async (
31673167
}
31683168
break
31693169
}
3170+
3171+
/**
3172+
* Saved Prompts
3173+
*/
3174+
3175+
case "requestSavedPrompts": {
3176+
const savedPrompts = getGlobalState("savedPrompts") || []
3177+
await provider.postMessageToWebview({
3178+
type: "savedPrompts",
3179+
savedPrompts,
3180+
})
3181+
break
3182+
}
3183+
3184+
case "createSavedPrompt": {
3185+
try {
3186+
const promptData = message.savedPromptCreate
3187+
if (!promptData) {
3188+
provider.log("Missing savedPromptCreate data")
3189+
break
3190+
}
3191+
3192+
const savedPrompts = getGlobalState("savedPrompts") || []
3193+
const newPrompt = {
3194+
...promptData,
3195+
id: `prompt-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
3196+
createdAt: Date.now(),
3197+
updatedAt: Date.now(),
3198+
}
3199+
3200+
await updateGlobalState("savedPrompts", [...savedPrompts, newPrompt])
3201+
await provider.postMessageToWebview({
3202+
type: "savedPrompts",
3203+
savedPrompts: [...savedPrompts, newPrompt],
3204+
})
3205+
} catch (error) {
3206+
provider.log(`Error creating saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3207+
}
3208+
break
3209+
}
3210+
3211+
case "updateSavedPrompt": {
3212+
try {
3213+
const promptUpdate = message.savedPromptUpdate
3214+
if (!promptUpdate?.id) {
3215+
provider.log("Missing savedPromptUpdate data or id")
3216+
break
3217+
}
3218+
3219+
const savedPrompts = getGlobalState("savedPrompts") || []
3220+
const updatedPrompts = savedPrompts.map((prompt: any) =>
3221+
prompt.id === promptUpdate.id
3222+
? { ...prompt, ...promptUpdate, updatedAt: Date.now() }
3223+
: prompt,
3224+
)
3225+
3226+
await updateGlobalState("savedPrompts", updatedPrompts)
3227+
await provider.postMessageToWebview({
3228+
type: "savedPrompts",
3229+
savedPrompts: updatedPrompts,
3230+
})
3231+
} catch (error) {
3232+
provider.log(`Error updating saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3233+
}
3234+
break
3235+
}
3236+
3237+
case "deleteSavedPrompt": {
3238+
try {
3239+
const promptId = message.savedPromptId
3240+
if (!promptId) {
3241+
provider.log("Missing savedPromptId")
3242+
break
3243+
}
3244+
3245+
const savedPrompts = getGlobalState("savedPrompts") || []
3246+
const filteredPrompts = savedPrompts.filter((prompt: any) => prompt.id !== promptId)
3247+
3248+
await updateGlobalState("savedPrompts", filteredPrompts)
3249+
await provider.postMessageToWebview({
3250+
type: "savedPrompts",
3251+
savedPrompts: filteredPrompts,
3252+
})
3253+
} catch (error) {
3254+
provider.log(`Error deleting saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3255+
}
3256+
break
3257+
}
3258+
3259+
case "exportSavedPrompts": {
3260+
try {
3261+
const savedPrompts = getGlobalState("savedPrompts") || []
3262+
const exportData = {
3263+
version: 1 as const,
3264+
exportedAt: Date.now(),
3265+
prompts: savedPrompts,
3266+
}
3267+
3268+
const defaultUri = await resolveDefaultSaveUri(provider, "savedPrompts", "json", "lastSavedPromptsExportPath")
3269+
const saveUri = await vscode.window.showSaveDialog({
3270+
defaultUri,
3271+
filters: { JSON: ["json"] },
3272+
title: t("common:savedPrompts.exportTitle"),
3273+
})
3274+
3275+
if (saveUri) {
3276+
await fs.writeFile(saveUri.fsPath, JSON.stringify(exportData, null, 2), "utf8")
3277+
vscode.window.showInformationMessage(t("common:savedPrompts.exportSuccess"))
3278+
await saveLastExportPath(provider, "lastSavedPromptsExportPath", saveUri.fsPath)
3279+
}
3280+
} catch (error) {
3281+
provider.log(`Error exporting saved prompts: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3282+
vscode.window.showErrorMessage(t("common:savedPrompts.exportError"))
3283+
}
3284+
break
3285+
}
3286+
3287+
case "importSavedPrompts": {
3288+
try {
3289+
const openUri = await vscode.window.showOpenDialog({
3290+
canSelectMany: false,
3291+
filters: { JSON: ["json"] },
3292+
title: t("common:savedPrompts.importTitle"),
3293+
})
3294+
3295+
if (openUri && openUri[0]) {
3296+
const content = await fs.readFile(openUri[0].fsPath, "utf8")
3297+
const importData = JSON.parse(content)
3298+
3299+
// Validate the import data structure
3300+
if (!importData.version || !Array.isArray(importData.prompts)) {
3301+
vscode.window.showErrorMessage(t("common:savedPrompts.invalidFormat"))
3302+
break
3303+
}
3304+
3305+
const existingPrompts = getGlobalState("savedPrompts") || []
3306+
3307+
// Merge prompts, avoiding duplicates by ID
3308+
const existingIds = new Set(existingPrompts.map((p: any) => p.id))
3309+
const newPrompts = importData.prompts.filter((p: any) => !existingIds.has(p.id))
3310+
const mergedPrompts = [...existingPrompts, ...newPrompts]
3311+
3312+
await updateGlobalState("savedPrompts", mergedPrompts)
3313+
await provider.postMessageToWebview({
3314+
type: "savedPrompts",
3315+
savedPrompts: mergedPrompts,
3316+
})
3317+
3318+
vscode.window.showInformationMessage(
3319+
t("common:savedPrompts.importSuccess", { count: newPrompts.length }),
3320+
)
3321+
}
3322+
} catch (error) {
3323+
provider.log(`Error importing saved prompts: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3324+
vscode.window.showErrorMessage(t("common:savedPrompts.importError"))
3325+
}
3326+
break
3327+
}
3328+
3329+
case "useSavedPrompt": {
3330+
try {
3331+
const promptId = message.savedPromptId
3332+
if (!promptId) {
3333+
provider.log("Missing savedPromptId")
3334+
break
3335+
}
3336+
3337+
const savedPrompts = getGlobalState("savedPrompts") || []
3338+
const prompt = savedPrompts.find((p: any) => p.id === promptId)
3339+
3340+
if (prompt) {
3341+
// If the prompt has an associated API config, switch to it
3342+
if (prompt.apiConfigId) {
3343+
const listApiConfigMeta = getGlobalState("listApiConfigMeta") || []
3344+
const targetConfig = listApiConfigMeta.find((config: any) => config.id === prompt.apiConfigId)
3345+
if (targetConfig) {
3346+
await updateGlobalState("currentApiConfigName", targetConfig.name)
3347+
await provider.postStateToWebview()
3348+
}
3349+
}
3350+
3351+
// Insert the prompt content into the textarea (replacing current content)
3352+
await provider.postMessageToWebview({
3353+
type: "insertTextIntoTextarea",
3354+
text: prompt.content,
3355+
})
3356+
}
3357+
} catch (error) {
3358+
provider.log(`Error using saved prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
3359+
}
3360+
break
3361+
}
3362+
31703363
case "showMdmAuthRequiredNotification": {
31713364
// Show notification that organization requires authentication
31723365
vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))

0 commit comments

Comments
 (0)