mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
ede1d29299
commit
0be1980362
10 changed files with 798 additions and 0 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
82
packages/types/src/saved-prompt.ts
Normal file
82
packages/types/src/saved-prompt.ts
Normal 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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
109
webview-ui/src/components/chat/SavedPromptsDropdown.tsx
Normal file
109
webview-ui/src/components/chat/SavedPromptsDropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
353
webview-ui/src/components/settings/SavedPromptsSettings.tsx
Normal file
353
webview-ui/src/components/settings/SavedPromptsSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue