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
This commit is contained in:
Roo Code 2026-02-02 15:03:06 +00:00
parent ede1d29299
commit 0be1980362
10 changed files with 798 additions and 0 deletions

View file

@ -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.
@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -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
@ -109,6 +110,7 @@ export interface ExtensionMessage {
| "branchWorktreeIncludeResult"
| "folderSelected"
| "skills"
| "savedPrompts"
text?: string
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
checkpointWarning?: {
@ -334,6 +336,7 @@ export type ExtensionState = Pick<
| "maxGitStatusFiles"
| "requestDelaySeconds"
| "showWorktreesInHomeScreen"
| "savedPrompts"
> & {
version: string
clineMessages: ClineMessage[]
@ -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"
@ -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 {

View file

@ -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"))
}
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"))

View file

@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from "react"
import { Bookmark, ChevronDown, Settings } from "lucide-react"
import type { SavedPrompt } from "@roo-code/types"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
StandardTooltip,
} from "@/components/ui"
import { vscode } from "@/utils/vscode"
interface SavedPromptsDropdownProps {
disabled?: boolean
}
export const SavedPromptsDropdown: React.FC<SavedPromptsDropdownProps> = ({ disabled }) => {
const { t } = useAppTranslation()
const { savedPrompts, listApiConfigMeta } = useExtensionState()
const [open, setOpen] = useState(false)
// Request saved prompts when component mounts
useEffect(() => {
vscode.postMessage({ type: "requestSavedPrompts" })
}, [])
const handleSelectPrompt = (prompt: SavedPrompt) => {
vscode.postMessage({
type: "useSavedPrompt",
savedPromptId: prompt.id,
})
setOpen(false)
}
const handleOpenSettings = () => {
vscode.postMessage({
type: "action",
action: "settingsButtonClicked",
})
// Note: User will need to navigate to Saved Prompts settings manually
setOpen(false)
}
const promptsList = savedPrompts || []
if (promptsList.length === 0) {
return null // Don't show the dropdown if there are no saved prompts
}
return (
<Popover open={open} onOpenChange={setOpen}>
<StandardTooltip content={t("chat:savedPrompts.tooltip")}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={disabled}
className="size-7 opacity-70 hover:opacity-100">
<Bookmark className="w-4 h-4" />
</Button>
</PopoverTrigger>
</StandardTooltip>
<PopoverContent className="w-64 p-0" align="end">
<div className="py-1">
<div className="px-3 py-2 text-xs font-medium text-vscode-descriptionForeground border-b border-vscode-panel-border">
{t("chat:savedPrompts.title")}
</div>
<div className="max-h-64 overflow-y-auto">
{promptsList.map((prompt: SavedPrompt) => {
const apiConfig = listApiConfigMeta?.find(
(config) => config.id === prompt.apiConfigId,
)
return (
<button
key={prompt.id}
onClick={() => handleSelectPrompt(prompt)}
className="w-full px-3 py-2 text-left hover:bg-vscode-list-hoverBackground flex flex-col gap-0.5">
<span className="text-sm font-medium truncate">{prompt.name}</span>
{prompt.description && (
<span className="text-xs text-vscode-descriptionForeground truncate">
{prompt.description}
</span>
)}
{apiConfig && (
<span className="text-xs text-vscode-textLink-foreground truncate">
{apiConfig.name}
</span>
)}
</button>
)
})}
</div>
<div className="border-t border-vscode-panel-border pt-1">
<button
onClick={handleOpenSettings}
className="w-full px-3 py-2 text-left hover:bg-vscode-list-hoverBackground flex items-center gap-2 text-sm text-vscode-descriptionForeground">
<Settings className="w-3.5 h-3.5" />
{t("chat:savedPrompts.managePrompts")}
</button>
</div>
</div>
</PopoverContent>
</Popover>
)
}

View file

@ -0,0 +1,353 @@
import React, { useState, useEffect } from "react"
import { Plus, Trash2, Edit2, Download, Upload, Bookmark } from "lucide-react"
import { Trans } from "react-i18next"
import type { SavedPrompt } from "@roo-code/types"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from "@/components/ui"
import { vscode } from "@/utils/vscode"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
interface SavedPromptFormData {
name: string
content: string
description: string
apiConfigId: string
}
const emptyFormData: SavedPromptFormData = {
name: "",
content: "",
description: "",
apiConfigId: "",
}
export const SavedPromptsSettings: React.FC = () => {
const { t } = useAppTranslation()
const { savedPrompts, listApiConfigMeta } = useExtensionState()
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [promptToDelete, setPromptToDelete] = useState<SavedPrompt | null>(null)
const [editDialogOpen, setEditDialogOpen] = useState(false)
const [editingPrompt, setEditingPrompt] = useState<SavedPrompt | null>(null)
const [formData, setFormData] = useState<SavedPromptFormData>(emptyFormData)
// Request saved prompts when component mounts
useEffect(() => {
vscode.postMessage({ type: "requestSavedPrompts" })
}, [])
const handleDeleteClick = (prompt: SavedPrompt) => {
setPromptToDelete(prompt)
setDeleteDialogOpen(true)
}
const handleDeleteConfirm = () => {
if (promptToDelete) {
vscode.postMessage({
type: "deleteSavedPrompt",
savedPromptId: promptToDelete.id,
})
setDeleteDialogOpen(false)
setPromptToDelete(null)
}
}
const handleDeleteCancel = () => {
setDeleteDialogOpen(false)
setPromptToDelete(null)
}
const handleEditClick = (prompt: SavedPrompt) => {
setEditingPrompt(prompt)
setFormData({
name: prompt.name,
content: prompt.content,
description: prompt.description || "",
apiConfigId: prompt.apiConfigId || "",
})
setEditDialogOpen(true)
}
const handleCreateClick = () => {
setEditingPrompt(null)
setFormData(emptyFormData)
setEditDialogOpen(true)
}
const handleSave = () => {
if (!formData.name.trim() || !formData.content.trim()) {
return
}
if (editingPrompt) {
// Update existing prompt
vscode.postMessage({
type: "updateSavedPrompt",
savedPromptUpdate: {
id: editingPrompt.id,
name: formData.name.trim(),
content: formData.content.trim(),
description: formData.description.trim() || undefined,
apiConfigId: formData.apiConfigId || undefined,
},
})
} else {
// Create new prompt
vscode.postMessage({
type: "createSavedPrompt",
savedPromptCreate: {
name: formData.name.trim(),
content: formData.content.trim(),
description: formData.description.trim() || undefined,
apiConfigId: formData.apiConfigId || undefined,
},
})
}
setEditDialogOpen(false)
setEditingPrompt(null)
setFormData(emptyFormData)
}
const handleExport = () => {
vscode.postMessage({ type: "exportSavedPrompts" })
}
const handleImport = () => {
vscode.postMessage({ type: "importSavedPrompts" })
}
const promptsList = savedPrompts || []
return (
<div>
<SectionHeader>{t("settings:sections.savedPrompts")}</SectionHeader>
<Section>
{/* Description section */}
<SearchableSetting
settingId="saved-prompts-description"
section="savedPrompts"
label={t("settings:sections.savedPrompts")}
className="mb-4">
<p className="text-sm text-vscode-descriptionForeground">
{t("settings:savedPrompts.description")}
</p>
</SearchableSetting>
{/* Action buttons */}
<div className="flex items-center gap-2 mb-4">
<Button variant="outline" size="sm" onClick={handleCreateClick}>
<Plus className="w-4 h-4 mr-1" />
{t("settings:savedPrompts.create")}
</Button>
<Button variant="outline" size="sm" onClick={handleImport}>
<Upload className="w-4 h-4 mr-1" />
{t("settings:savedPrompts.import")}
</Button>
{promptsList.length > 0 && (
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="w-4 h-4 mr-1" />
{t("settings:savedPrompts.export")}
</Button>
)}
</div>
{/* Saved prompts list */}
<SearchableSetting
settingId="saved-prompts-list"
section="savedPrompts"
label={t("settings:savedPrompts.listTitle")}
className="mb-6">
<div className="flex items-center gap-1.5 mb-2">
<Bookmark className="w-3 h-3" />
<h4 className="text-sm font-medium m-0">{t("settings:savedPrompts.listTitle")}</h4>
</div>
<div className="border border-vscode-panel-border rounded-md">
{promptsList.length === 0 ? (
<div className="px-4 py-3 text-sm text-vscode-descriptionForeground">
{t("settings:savedPrompts.noPrompts")}
</div>
) : (
promptsList.map((prompt: SavedPrompt) => {
const apiConfig = listApiConfigMeta?.find(
(config) => config.id === prompt.apiConfigId,
)
return (
<div
key={prompt.id}
className="px-4 py-2 flex items-center justify-between hover:bg-vscode-list-hoverBackground border-b border-vscode-panel-border last:border-b-0">
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{prompt.name}</div>
{prompt.description && (
<div className="text-xs text-vscode-descriptionForeground truncate">
{prompt.description}
</div>
)}
{apiConfig && (
<div className="text-xs text-vscode-descriptionForeground mt-1">
{t("settings:savedPrompts.apiConfig")}: {apiConfig.name}
</div>
)}
</div>
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditClick(prompt)}
className="size-6 opacity-60 hover:opacity-100">
<Edit2 className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(prompt)}
className="size-6 opacity-60 hover:opacity-100 text-vscode-errorForeground">
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</div>
)
})
)}
</div>
</SearchableSetting>
</Section>
{/* Delete confirmation dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("settings:savedPrompts.deleteTitle")}</AlertDialogTitle>
<AlertDialogDescription>
{t("settings:savedPrompts.deleteDescription", { name: promptToDelete?.name })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleDeleteCancel}>
{t("common:cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}>
{t("common:delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Edit/Create dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{editingPrompt
? t("settings:savedPrompts.editTitle")
: t("settings:savedPrompts.createTitle")}
</DialogTitle>
<DialogDescription>
{editingPrompt
? t("settings:savedPrompts.editDescription")
: t("settings:savedPrompts.createDescription")}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<label htmlFor="name" className="text-sm font-medium">
{t("settings:savedPrompts.nameLabel")} *
</label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t("settings:savedPrompts.namePlaceholder")}
/>
</div>
<div className="grid gap-2">
<label htmlFor="description" className="text-sm font-medium">
{t("settings:savedPrompts.descriptionLabel")}
</label>
<Input
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder={t("settings:savedPrompts.descriptionPlaceholder")}
/>
</div>
<div className="grid gap-2">
<label htmlFor="content" className="text-sm font-medium">
{t("settings:savedPrompts.contentLabel")} *
</label>
<Textarea
id="content"
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder={t("settings:savedPrompts.contentPlaceholder")}
rows={6}
/>
</div>
<div className="grid gap-2">
<label htmlFor="apiConfig" className="text-sm font-medium">
{t("settings:savedPrompts.apiConfigLabel")}
</label>
<Select
value={formData.apiConfigId}
onValueChange={(value) => setFormData({ ...formData, apiConfigId: value })}>
<SelectTrigger>
<SelectValue placeholder={t("settings:savedPrompts.apiConfigPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{t("settings:savedPrompts.noApiConfig")}</SelectItem>
{listApiConfigMeta?.map((config) => (
<SelectItem key={config.id} value={config.id}>
{config.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-vscode-descriptionForeground">
{t("settings:savedPrompts.apiConfigDescription")}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
{t("common:cancel")}
</Button>
<Button
onClick={handleSave}
disabled={!formData.name.trim() || !formData.content.trim()}>
{editingPrompt ? t("common:save") : t("common:create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View file

@ -273,6 +273,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
const [commands, setCommands] = useState<Command[]>([])
const [skills, setSkills] = useState<SkillMetadata[]>([])
const [savedPrompts, setSavedPrompts] = useState<any[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
const [extensionRouterModels, setExtensionRouterModels] = useState<RouterModels | undefined>(undefined)
@ -375,6 +376,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setSkills(message.skills ?? [])
break
}
case "savedPrompts": {
setSavedPrompts(message.savedPrompts ?? [])
break
}
case "messageUpdated": {
const clineMessage = message.clineMessage!
setState((prevState) => {
@ -488,6 +493,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openedTabs,
commands,
skills,
savedPrompts,
soundVolume: state.soundVolume,
ttsSpeed: state.ttsSpeed,
writeDelayMs: state.writeDelayMs,

View file

@ -469,6 +469,11 @@
"confirm": "Delete"
}
},
"savedPrompts": {
"tooltip": "Saved Prompts",
"title": "Saved Prompts",
"managePrompts": "Manage Prompts..."
},
"slashCommand": {
"wantsToRun": "Roo wants to run a slash command",
"didRun": "Roo ran a slash command"

View file

@ -38,6 +38,7 @@
"contextManagement": "Context",
"terminal": "Terminal",
"slashCommands": "Slash Commands",
"savedPrompts": "Saved Prompts",
"prompts": "Prompts",
"ui": "UI",
"experimental": "Experimental",
@ -71,6 +72,31 @@
"slashCommands": {
"description": "Manage your slash commands to quickly execute custom workflows and actions. <DocsLink>Learn more</DocsLink>"
},
"savedPrompts": {
"description": "Create and manage saved prompts for quick access during chat. Associate prompts with specific API configurations for automatic switching.",
"create": "Create",
"import": "Import",
"export": "Export",
"listTitle": "Your Saved Prompts",
"noPrompts": "No saved prompts yet. Create one to get started.",
"apiConfig": "API Config",
"deleteTitle": "Delete Saved Prompt",
"deleteDescription": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.",
"editTitle": "Edit Saved Prompt",
"editDescription": "Update your saved prompt details.",
"createTitle": "Create Saved Prompt",
"createDescription": "Create a new saved prompt for quick access during chat.",
"nameLabel": "Name",
"namePlaceholder": "e.g., Code Review, Bug Fix, etc.",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Optional description for this prompt",
"contentLabel": "Prompt Content",
"contentPlaceholder": "Enter your prompt content here...",
"apiConfigLabel": "API Configuration",
"apiConfigPlaceholder": "Select an API configuration",
"noApiConfig": "None (use current)",
"apiConfigDescription": "When selected, the API configuration will automatically switch when using this prompt."
},
"skills": {
"description": "Manage skills that provide contextual instructions to the agent. Skills are automatically applied when relevant to your tasks. <DocsLink>Learn more</DocsLink>",
"projectSkills": "Project Skills",