Files
Roo-Code/webview-ui/src/components/prompts/PromptsView.tsx
Matt Rubens b8e0aa0cde Custom modes
2025-01-21 09:39:54 -05:00

1122 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo, useCallback } from "react"
import {
VSCodeButton,
VSCodeTextArea,
VSCodeDropdown,
VSCodeOption,
VSCodeTextField,
VSCodeCheckbox,
} from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
Mode,
PromptComponent,
getRoleDefinition,
getAllModes,
ModeConfig,
enhancePrompt,
} from "../../../../src/shared/modes"
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
import { vscode } from "../../utils/vscode"
// Get all available groups from GROUP_DISPLAY_NAMES
const availableGroups = Object.keys(TOOL_GROUPS) as ToolGroup[]
type PromptsViewProps = {
onDone: () => void
}
const PromptsView = ({ onDone }: PromptsViewProps) => {
const {
customPrompts,
listApiConfigMeta,
enhancementApiConfigId,
setEnhancementApiConfigId,
mode,
customInstructions,
setCustomInstructions,
preferredLanguage,
setPreferredLanguage,
customModes,
} = useExtensionState()
// Memoize modes to preserve array order
const modes = useMemo(() => getAllModes(customModes), [customModes])
const [testPrompt, setTestPrompt] = useState("")
const [isEnhancing, setIsEnhancing] = useState(false)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [selectedPromptContent, setSelectedPromptContent] = useState("")
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
// Direct update functions
const updateAgentPrompt = useCallback(
(mode: Mode, promptData: PromptComponent) => {
const existingPrompt = customPrompts?.[mode]
const updatedPrompt = { ...existingPrompt, ...promptData }
// Only include properties that differ from defaults
if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) {
delete updatedPrompt.roleDefinition
}
vscode.postMessage({
type: "updatePrompt",
promptMode: mode,
customPrompt: updatedPrompt,
})
},
[customPrompts],
)
const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => {
vscode.postMessage({
type: "updateCustomMode",
slug,
modeConfig,
})
}, [])
// Helper function to find a mode by slug
const findModeBySlug = useCallback(
(searchSlug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined => {
if (!modes) return undefined
const isModeWithSlug = (mode: ModeConfig): mode is ModeConfig => mode.slug === searchSlug
return modes.find(isModeWithSlug)
},
[],
)
const switchMode = useCallback((slug: string) => {
vscode.postMessage({
type: "mode",
text: slug,
})
}, [])
// Handle mode switching with explicit state initialization
const handleModeSwitch = useCallback(
(modeConfig: ModeConfig) => {
if (modeConfig.slug === mode) return // Prevent unnecessary updates
// First switch the mode
switchMode(modeConfig.slug)
// Exit tools edit mode when switching modes
setIsToolsEditMode(false)
},
[mode, switchMode, setIsToolsEditMode],
)
// Helper function to get current mode's config
const getCurrentMode = useCallback((): ModeConfig | undefined => {
const findMode = (m: ModeConfig): boolean => m.slug === mode
return customModes?.find(findMode) || modes.find(findMode)
}, [mode, customModes, modes])
// Helper function to safely access mode properties
const getModeProperty = <T extends keyof ModeConfig>(
mode: ModeConfig | undefined,
property: T,
): ModeConfig[T] | undefined => {
return mode?.[property]
}
// State for create mode dialog
const [newModeName, setNewModeName] = useState("")
const [newModeSlug, setNewModeSlug] = useState("")
const [newModeRoleDefinition, setNewModeRoleDefinition] = useState("")
const [newModeCustomInstructions, setNewModeCustomInstructions] = useState("")
const [newModeGroups, setNewModeGroups] = useState<readonly ToolGroup[]>(availableGroups)
// Reset form fields when dialog opens
useEffect(() => {
if (isCreateModeDialogOpen) {
setNewModeGroups(availableGroups)
setNewModeRoleDefinition("")
setNewModeCustomInstructions("")
}
}, [isCreateModeDialogOpen])
// Helper function to generate a unique slug from a name
const generateSlug = useCallback((name: string, attempt = 0): string => {
const baseSlug = name
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "")
return attempt === 0 ? baseSlug : `${baseSlug}-${attempt}`
}, [])
// Handler for name changes
const handleNameChange = useCallback(
(name: string) => {
setNewModeName(name)
setNewModeSlug(generateSlug(name))
},
[generateSlug],
)
const handleCreateMode = useCallback(() => {
if (!newModeName.trim() || !newModeSlug.trim()) return
const newMode: ModeConfig = {
slug: newModeSlug,
name: newModeName,
roleDefinition: newModeRoleDefinition.trim() || "",
customInstructions: newModeCustomInstructions.trim() || undefined,
groups: newModeGroups,
}
updateCustomMode(newModeSlug, newMode)
switchMode(newModeSlug)
setIsCreateModeDialogOpen(false)
setNewModeName("")
setNewModeSlug("")
setNewModeRoleDefinition("")
setNewModeCustomInstructions("")
setNewModeGroups(availableGroups)
}, [
newModeName,
newModeSlug,
newModeRoleDefinition,
newModeCustomInstructions,
newModeGroups,
updateCustomMode,
switchMode,
])
const isNameOrSlugTaken = useCallback(
(name: string, slug: string) => {
return modes.some((m) => m.slug === slug || m.name === name)
},
[modes],
)
const openCreateModeDialog = useCallback(() => {
const baseNamePrefix = "New Custom Mode"
// Find unique name and slug
let attempt = 0
let name = baseNamePrefix
let slug = generateSlug(name)
while (isNameOrSlugTaken(name, slug)) {
attempt++
name = `${baseNamePrefix} ${attempt + 1}`
slug = generateSlug(name)
}
setNewModeName(name)
setNewModeSlug(slug)
setIsCreateModeDialogOpen(true)
}, [generateSlug, isNameOrSlugTaken])
// Handler for group checkbox changes
const handleGroupChange = useCallback(
(group: ToolGroup, isCustomMode: boolean, customMode: ModeConfig | undefined) =>
(e: Event | React.FormEvent<HTMLElement>) => {
if (!isCustomMode) return // Prevent changes to built-in modes
const target = (e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)
const checked = target.checked
const oldGroups = customMode?.groups || []
let newGroups: readonly ToolGroup[]
if (checked) {
newGroups = [...oldGroups, group]
} else {
newGroups = oldGroups.filter((g) => g !== group)
}
if (customMode) {
updateCustomMode(customMode.slug, {
...customMode,
groups: newGroups,
})
}
},
[updateCustomMode],
)
useEffect(() => {
const handler = (event: MessageEvent) => {
const message = event.data
if (message.type === "enhancedPrompt") {
if (message.text) {
setTestPrompt(message.text)
}
setIsEnhancing(false)
} else if (message.type === "systemPrompt") {
if (message.text) {
setSelectedPromptContent(message.text)
setSelectedPromptTitle(`System Prompt (${message.mode} mode)`)
setIsDialogOpen(true)
}
}
}
window.addEventListener("message", handler)
return () => window.removeEventListener("message", handler)
}, [])
const updateEnhancePrompt = (value: string | undefined) => {
vscode.postMessage({
type: "updateEnhancedPrompt",
text: value,
})
}
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>): void => {
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
const trimmedValue = value.trim()
if (trimmedValue !== enhancePrompt.default) {
updateEnhancePrompt(trimmedValue || enhancePrompt.default)
}
}
const handleAgentReset = (modeSlug: string) => {
// Only reset role definition for built-in modes
const existingPrompt = customPrompts?.[modeSlug]
updateAgentPrompt(modeSlug, {
...existingPrompt,
roleDefinition: undefined,
})
}
const handleEnhanceReset = () => {
updateEnhancePrompt(undefined)
}
const getEnhancePromptValue = (): string => {
return enhancePrompt.get(customPrompts)
}
const handleTestEnhancement = () => {
if (!testPrompt.trim()) return
setIsEnhancing(true)
vscode.postMessage({
type: "enhancePrompt",
text: testPrompt,
})
}
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
flexDirection: "column",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 17px 10px 20px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Prompts</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
<div style={{ marginBottom: "20px" }}>
<div style={{ marginBottom: "20px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Preferred Language</div>
<select
value={preferredLanguage}
onChange={(e) => {
setPreferredLanguage(e.target.value)
vscode.postMessage({
type: "preferredLanguage",
text: e.target.value,
})
}}
style={{
width: "100%",
padding: "4px 8px",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
height: "28px",
}}>
<option value="English">English</option>
<option value="Arabic">Arabic - العربية</option>
<option value="Brazilian Portuguese">Portuguese - Português (Brasil)</option>
<option value="Czech">Czech - Čeština</option>
<option value="French">French - Français</option>
<option value="German">German - Deutsch</option>
<option value="Hindi">Hindi - ि</option>
<option value="Hungarian">Hungarian - Magyar</option>
<option value="Italian">Italian - Italiano</option>
<option value="Japanese">Japanese - </option>
<option value="Korean">Korean - </option>
<option value="Polish">Polish - Polski</option>
<option value="Portuguese">Portuguese - Português (Portugal)</option>
<option value="Russian">Russian - Русский</option>
<option value="Simplified Chinese">Simplified Chinese - </option>
<option value="Spanish">Spanish - Español</option>
<option value="Traditional Chinese">Traditional Chinese - </option>
<option value="Turkish">Turkish - Türkçe</option>
</select>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the language that Cline should use for communication.
</p>
</div>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions for All Modes</div>
<div
style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced
by mode-specific instructions below.
</div>
<VSCodeTextArea
value={customInstructions ?? ""}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
setCustomInstructions(value || undefined)
vscode.postMessage({
type: "customInstructions",
text: value.trim() || undefined,
})
}}
rows={4}
resize="vertical"
style={{ width: "100%" }}
data-testid="global-custom-instructions-textarea"
/>
<div style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", marginTop: "5px" }}>
Instructions can also be loaded from{" "}
<span
style={{
color: "var(--vscode-textLink-foreground)",
cursor: "pointer",
textDecoration: "underline",
}}
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.clinerules",
values: {
create: true,
content: "",
},
})
}>
.clinerules
</span>{" "}
in your workspace.
</div>
</div>
<div style={{ marginBottom: "20px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "12px",
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Mode-Specific Prompts</h3>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton appearance="icon" onClick={openCreateModeDialog} title="Create new mode">
<span className="codicon codicon-add"></span>
</VSCodeButton>
<VSCodeButton
appearance="icon"
title="Edit modes configuration"
onClick={() => {
vscode.postMessage({
type: "openFile",
text: "settings/cline_custom_modes.json",
})
}}>
<span className="codicon codicon-json"></span>
</VSCodeButton>
</div>
</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "12px",
}}>
Hit the + to create a new custom mode, or just ask Roo in chat to create one for you!
</div>
<div
style={{
display: "flex",
gap: "16px",
alignItems: "center",
marginBottom: "12px",
overflowX: "auto",
flexWrap: "nowrap",
paddingBottom: "4px",
paddingRight: "20px",
}}>
{modes.map((modeConfig) => {
const isActive = mode === modeConfig.slug
return (
<button
key={modeConfig.slug}
data-testid={`${modeConfig.slug}-tab`}
data-active={isActive ? "true" : "false"}
onClick={() => handleModeSwitch(modeConfig)}
style={{
padding: "4px 8px",
border: "none",
background: isActive ? "var(--vscode-button-background)" : "none",
color: isActive
? "var(--vscode-button-foreground)"
: "var(--vscode-foreground)",
cursor: "pointer",
opacity: isActive ? 1 : 0.8,
borderRadius: "3px",
fontWeight: "bold",
}}>
{modeConfig.name}
</button>
)
})}
</div>
</div>
<div style={{ marginBottom: "20px" }}>
{/* Only show name and delete for custom modes */}
{mode && findModeBySlug(mode, customModes) && (
<div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeTextField
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
updateCustomMode(mode, {
...customMode,
name: target.value,
})
}
}}
style={{ width: "100%" }}
/>
<VSCodeButton
appearance="icon"
title="Delete mode"
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: mode,
})
}}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</div>
</div>
</div>
)}
<div style={{ marginBottom: "16px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Role Definition</div>
{!findModeBySlug(mode, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug)
}
}}
title="Reset to default"
data-testid="role-definition-reset">
<span className="codicon codicon-discard"></span>
</VSCodeButton>
)}
</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Define Roo's expertise and personality for this mode. This description shapes how Roo
presents itself and approaches tasks.
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode]
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
...customMode,
roleDefinition: value.trim() || "",
})
} else {
// For built-in modes, update the prompts
updateAgentPrompt(mode, {
roleDefinition: value.trim() || undefined,
})
}
}}
rows={4}
resize="vertical"
style={{ width: "100%" }}
data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`}
/>
</div>
{/* Mode settings */}
<>
{/* Show tools for all modes */}
<div style={{ marginBottom: "16px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Available Tools</div>
{findModeBySlug(mode, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => setIsToolsEditMode(!isToolsEditMode)}
title={isToolsEditMode ? "Done editing" : "Edit tools"}>
<span
className={`codicon codicon-${isToolsEditMode ? "check" : "edit"}`}></span>
</VSCodeButton>
)}
</div>
{!findModeBySlug(mode, customModes) && (
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Tools for built-in modes cannot be modified
</div>
)}
{isToolsEditMode && findModeBySlug(mode, customModes) ? (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: "8px",
}}>
{availableGroups.map((group) => {
const currentMode = getCurrentMode()
const isCustomMode = findModeBySlug(mode, customModes)
const customMode = isCustomMode
const isGroupEnabled = isCustomMode
? customMode?.groups?.includes(group)
: currentMode?.groups?.includes(group)
return (
<VSCodeCheckbox
key={group}
checked={isGroupEnabled}
onChange={handleGroupChange(group, Boolean(isCustomMode), customMode)}
disabled={!isCustomMode}>
{GROUP_DISPLAY_NAMES[group]}
</VSCodeCheckbox>
)
})}
</div>
) : (
<div
style={{
fontSize: "13px",
color: "var(--vscode-foreground)",
marginBottom: "8px",
lineHeight: "1.4",
}}>
{(() => {
const currentMode = getCurrentMode()
const enabledGroups = currentMode?.groups || []
return enabledGroups.map((group) => GROUP_DISPLAY_NAMES[group]).join(", ")
})()}
</div>
)}
</div>
</>
{/* Role definition for both built-in and custom modes */}
<div style={{ marginBottom: "8px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Add behavioral guidelines specific to {getCurrentMode()?.name || "Code"} mode.
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customPrompts?.[mode]
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
...customMode,
customInstructions: value.trim() || undefined,
})
} else {
// For built-in modes, update the prompts
const existingPrompt = customPrompts?.[mode]
updateAgentPrompt(mode, {
...existingPrompt,
customInstructions: value.trim() || undefined,
})
}
}}
rows={4}
resize="vertical"
style={{ width: "100%" }}
data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`}
/>
<div
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginTop: "5px",
}}>
Custom instructions specific to {getCurrentMode()?.name || "Code"} mode can also be loaded
from{" "}
<span
style={{
color: "var(--vscode-textLink-foreground)",
cursor: "pointer",
textDecoration: "underline",
}}
onClick={() => {
const currentMode = getCurrentMode()
if (!currentMode) return
// Open or create an empty file
vscode.postMessage({
type: "openFile",
text: `./.clinerules-${currentMode.slug}`,
values: {
create: true,
content: "",
},
})
}}>
.clinerules-{getCurrentMode()?.slug || "code"}
</span>{" "}
in your workspace.
</div>
</div>
</div>
<div style={{ marginBottom: "20px", display: "flex", justifyContent: "flex-start" }}>
<VSCodeButton
appearance="primary"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode) {
vscode.postMessage({
type: "getSystemPrompt",
mode: currentMode.slug,
})
}
}}
data-testid="preview-prompt-button">
Preview System Prompt
</VSCodeButton>
</div>
<h3 style={{ color: "var(--vscode-foreground)", margin: "40px 0 20px 0" }}>Prompt Enhancement</h3>
<div
style={{
color: "var(--vscode-foreground)",
fontSize: "13px",
marginBottom: "20px",
marginTop: "5px",
}}>
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo
understands your intent and provides the best possible responses.
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div>
<div style={{ marginBottom: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>API Configuration</div>
<div style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)" }}>
You can select an API configuration to always use for enhancing prompts, or just use
whatever is currently selected
</div>
</div>
<VSCodeDropdown
value={enhancementApiConfigId || ""}
data-testid="api-config-dropdown"
onChange={(e: any) => {
const value = e.detail?.target?.value || e.target?.value
setEnhancementApiConfigId(value)
vscode.postMessage({
type: "enhancementApiConfigId",
text: value,
})
}}
style={{ width: "300px" }}>
<VSCodeOption value="">Use currently selected API configuration</VSCodeOption>
{(listApiConfigMeta || []).map((config) => (
<VSCodeOption key={config.id} value={config.id}>
{config.name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<div style={{ marginBottom: "8px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Enhancement Prompt</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeButton
appearance="icon"
onClick={handleEnhanceReset}
title="Revert to default">
<span className="codicon codicon-discard"></span>
</VSCodeButton>
</div>
</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
This prompt will be used to refine your input when you hit the sparkle icon in chat.
</div>
</div>
<VSCodeTextArea
value={getEnhancePromptValue()}
onChange={handleEnhancePromptChange}
rows={4}
resize="vertical"
style={{ width: "100%" }}
/>
<div style={{ marginTop: "12px" }}>
<VSCodeTextArea
value={testPrompt}
onChange={(e) => setTestPrompt((e.target as HTMLTextAreaElement).value)}
placeholder="Enter a prompt to test the enhancement"
rows={3}
resize="vertical"
style={{ width: "100%" }}
data-testid="test-prompt-textarea"
/>
<div
style={{
marginTop: "8px",
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
gap: 8,
}}>
<VSCodeButton
onClick={handleTestEnhancement}
disabled={isEnhancing}
appearance="primary">
Preview Prompt Enhancement
</VSCodeButton>
</div>
</div>
</div>
</div>
{/* Bottom padding */}
<div style={{ height: "20px" }} />
</div>
{isCreateModeDialogOpen && (
<div
style={{
position: "fixed",
inset: 0,
display: "flex",
justifyContent: "flex-end",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 1000,
}}>
<div
style={{
width: "calc(100vw - 100px)",
height: "100%",
backgroundColor: "var(--vscode-editor-background)",
boxShadow: "-2px 0 5px rgba(0, 0, 0, 0.2)",
display: "flex",
flexDirection: "column",
position: "relative",
}}>
<div
style={{
flex: 1,
padding: "20px",
overflowY: "auto",
minHeight: 0,
}}>
<VSCodeButton
appearance="icon"
onClick={() => setIsCreateModeDialogOpen(false)}
style={{
position: "absolute",
top: "20px",
right: "20px",
}}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h2 style={{ margin: "0 0 16px" }}>Create New Mode</h2>
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
<VSCodeTextField
value={newModeName}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
handleNameChange(target.value)
}}
style={{ width: "100%" }}
/>
</div>
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Slug</div>
<VSCodeTextField
value={newModeSlug}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
setNewModeSlug(target.value)
}}
style={{ width: "100%" }}
/>
<div
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginTop: "4px",
}}>
The slug is used in URLs and file names. It should be lowercase and contain only
letters, numbers, and hyphens.
</div>
</div>
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Role Definition</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Define Roo's expertise and personality for this mode.
</div>
<VSCodeTextArea
value={newModeRoleDefinition}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
setNewModeRoleDefinition(value)
}}
rows={4}
resize="vertical"
style={{ width: "100%" }}
/>
</div>
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Available Tools</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Select which tools this mode can use.
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: "8px",
}}>
{availableGroups.map((group) => (
<VSCodeCheckbox
key={group}
checked={newModeGroups.includes(group)}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)
const checked = target.checked
if (checked) {
setNewModeGroups([...newModeGroups, group])
} else {
setNewModeGroups(newModeGroups.filter((g) => g !== group))
}
}}>
{GROUP_DISPLAY_NAMES[group]}
</VSCodeCheckbox>
))}
</div>
</div>
<div style={{ marginBottom: "16px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions</div>
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
}}>
Add behavioral guidelines specific to this mode.
</div>
<VSCodeTextArea
value={newModeCustomInstructions}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
setNewModeCustomInstructions(value)
}}
rows={4}
resize="vertical"
style={{ width: "100%" }}
/>
</div>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
padding: "12px 20px",
gap: "8px",
borderTop: "1px solid var(--vscode-editor-lineHighlightBorder)",
backgroundColor: "var(--vscode-editor-background)",
}}>
<VSCodeButton onClick={() => setIsCreateModeDialogOpen(false)}>Cancel</VSCodeButton>
<VSCodeButton
appearance="primary"
onClick={handleCreateMode}
disabled={!newModeName.trim() || !newModeSlug.trim()}>
Create Mode
</VSCodeButton>
</div>
</div>
</div>
)}
{isDialogOpen && (
<div
style={{
position: "fixed",
inset: 0,
display: "flex",
justifyContent: "flex-end",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 1000,
}}>
<div
style={{
width: "calc(100vw - 100px)",
height: "100%",
backgroundColor: "var(--vscode-editor-background)",
boxShadow: "-2px 0 5px rgba(0, 0, 0, 0.2)",
display: "flex",
flexDirection: "column",
position: "relative",
}}>
<div
style={{
flex: 1,
padding: "20px",
overflowY: "auto",
minHeight: 0,
}}>
<VSCodeButton
appearance="icon"
onClick={() => setIsDialogOpen(false)}
style={{
position: "absolute",
top: "20px",
right: "20px",
}}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
<h2 style={{ margin: "0 0 16px" }}>{selectedPromptTitle}</h2>
<pre
style={{
padding: "8px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
fontFamily: "var(--vscode-editor-font-family)",
fontSize: "var(--vscode-editor-font-size)",
color: "var(--vscode-editor-foreground)",
backgroundColor: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-editor-lineHighlightBorder)",
borderRadius: "4px",
overflowY: "auto",
}}>
{selectedPromptContent}
</pre>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
padding: "12px 20px",
borderTop: "1px solid var(--vscode-editor-lineHighlightBorder)",
backgroundColor: "var(--vscode-editor-background)",
}}>
<VSCodeButton onClick={() => setIsDialogOpen(false)}>Close</VSCodeButton>
</div>
</div>
</div>
)}
</div>
)
}
export default PromptsView