mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-19 00:01:19 +00:00
* fix: cancel backend auto-approval timeout when auto-approve is toggled off mid-countdown (#11439) Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> * fix: prevent chat history loss during cloud/settings navigation (#11371) (#11372) Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> * fix: preserve pasted images in chatbox during chat activity (#11375) Co-authored-by: Roo Code <roomote@roocode.com> * fix: resolve chat scroll anchoring and task-switch scroll race condit… (#11385) * fix: avoid zsh process-substitution false positives in assignments (#11365) * fix(editor): make tab close best-effort in DiffViewProvider.open (#11363) * fix(checkpoints): canonicalize core.worktree comparison to prevent Windows path mismatch failures (#11346) * fix: prevent double notification sound playback (#11283) * fix: prevent false unsaved changes prompt with OpenAI Compatible headers (#8230) (#11334) fix: prevent false unsaved changes prompt with OpenAI Compatible headers Mark automatic header syncs in ApiOptions and OpenAICompatible as non-user actions (isUserAction: false) and enhance SettingsView change detection to skip automatic syncs with semantically equal values. Root cause: two components (ApiOptions and OpenAICompatible) manage openAiHeaders state and automatically sync it back on mount/remount. These syncs were treated as user changes, triggering a false dirty state. Co-authored-by: Robert McIntyre <robertjmcintyre@users.noreply.github.com> * fix: remove noisy console.warn logs from NativeToolCallParser (#11264) Remove two console.warn messages that fire excessively when loading tasks from history: - 'Attempting to finalize unknown tool call' in finalizeStreamingToolCall() - 'Received chunk for unknown tool call' in processStreamingChunk() The defensive null-return behavior is preserved; only the log output is removed. * refactor: remove footgun prompting (file-based system prompt override) (#11387) * refactor: delete orphaned per-provider caching transform files (#11388) * feat: add disabledTools setting to globally disable native tools (#11277) * feat: add disabledTools setting to globally disable native tools Add a disabledTools field to GlobalSettings that allows disabling specific native tools by name. This enables cloud agents to be configured with restricted tool access. Schema: - Add disabledTools: z.array(toolNamesSchema).optional() to globalSettingsSchema - Add disabledTools to organizationDefaultSettingsSchema.pick() - Add disabledTools to ExtensionState Pick type Prompt generation (tool filtering): - Add disabledTools to BuildToolsOptions interface - Pass disabledTools through filterSettings to filterNativeToolsForMode() - Remove disabled tools from allowedToolNames set in filterNativeToolsForMode() Execution-time validation (safety net): - Extract disabledTools from state in presentAssistantMessage - Convert disabledTools to toolRequirements format for validateToolUse() Wiring: - Add disabledTools to ClineProvider getState() and getStateToPostToWebview() - Pass disabledTools to all buildNativeToolsArrayWithRestrictions() call sites EXT-778 * fix: check toolRequirements before ALWAYS_AVAILABLE_TOOLS Moves the toolRequirements check before the ALWAYS_AVAILABLE_TOOLS early-return in isToolAllowedForMode(). This ensures disabledTools can block always-available tools (switch_mode, new_task, etc.) at execution time, making the validation layer consistent with the filtering layer. * feat: add support for .agents/skills directory (#11181) * feat: add support for .agents/skills directory This change adds support for discovering skills from the .agents/skills directory, following the Agent Skills convention for sharing skills across different AI coding tools. Priority order (later entries override earlier ones): 1. Global ~/.agents/skills (shared across AI coding tools, lowest priority) 2. Project .agents/skills 3. Global ~/.roo/skills (Roo-specific) 4. Project .roo/skills (highest priority) Changes: - Add getGlobalAgentsDirectory() and getProjectAgentsDirectoryForCwd() functions to roo-config - Update SkillsManager.getSkillsDirectories() to include .agents/skills - Update SkillsManager.setupFileWatchers() to watch .agents/skills - Add tests for new functionality * fix: clarify skill priority comment to match actual behavior * fix: clarify skill priority comment to explain Map.set replacement mechanism --------- Co-authored-by: Roo Code <roomote@roocode.com> * feat(history): render nested subtasks as recursive tree (#11299) * feat(history): render nested subtasks as recursive tree * fix(lockfile): resolve missing ai-sdk provider entry * fix: address review feedback — dedupe countAll, increase SubtaskRow max-h - HistoryView: replace local countAll with imported countAllSubtasks from types.ts - SubtaskRow: increase nested children max-h from 500px to 2000px to match TaskGroupItem * perf(refactor): consolidate getState calls in resolveWebviewView (#11320) * perf(refactor): consolidate getState calls in resolveWebviewView Replace three separate this.getState().then() calls with a single await this.getState() and destructuring. This avoids running the full getState() method (CloudService calls, ContextProxy reads, etc.) three times during webview view resolution. * fix: keep getState consolidation non-blocking to avoid delaying webview render --------- Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> * fix: harden command auto-approval against inline JS false positives (#11382) * feat: rename search_and_replace tool to edit and unify edit-family UI (#11296) * Revert "refactor: delete orphaned per-provider caching transform files (#11388)" This reverts commit13a45b0361. * chore: regenerate built-in-skills.ts with updated formatting * fix: add missing maxReadFileLine property to test baseState The ExtensionState type now requires maxReadFileLine property (added in commit63e3f769a). Update the test to include this property with the default value of -1 (unlimited reading). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add pnpm serve command for code-server development (#10964) Co-authored-by: Roo Code <roomote@roocode.com> * chore: remove Feature Request from issue template options (#11141) Co-authored-by: Roo Code <roomote@roocode.com> * refactor(docs-extractor): simplify mode to focus on raw fact extraction (#11129) * Add cli support for linux (#11167) * fix: replace heredocs with echo statements in cli-release workflow (#11168) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Drop MacOS-13 cli support (#11169) * fix(cli): correct example in install script (#11170) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat: add Kimi K2.5 model to Fireworks provider (#11177) * feat(cli): improve dev experience and roo provider API key support (#11203) - Allow --api-key and ROO_API_KEY env var for the roo provider instead of requiring cloud auth token - Switch dev/start scripts to use tsx for running directly from source without building first - Fix path resolution (version.ts, extension.ts, extension-host.ts) to work from both source and bundled locations - Disable debug log file (~/.roo/cli-debug.log) unless --debug is passed - Update README with complete env var table and dev workflow docs Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Roo Code CLI v0.0.50 (#11204) * Roo Code CLI v0.0.50 * docs(cli): add --exit-on-error to changelog --------- Co-authored-by: Roo Code <roomote@roocode.com> * feat(cli): update default model from Opus 4.5 to Opus 4.6 (#11273) Co-authored-by: Roo Code <roomote@roocode.com> * feat(web): replace Roomote Control with Linear Integration in cloud features grid (#11280) Co-authored-by: Roo Code <roomote@roocode.com> * Add linux-arm64 for the roo cli (#11314) * chore: clean up repo-facing mode rules (#11410) * Make CLI auto-approve by default with require-approval opt-in (#11424) Co-authored-by: Roo Code <roomote@roocode.com> * Add new code owners to CODEOWNERS file * Update next.js (#11108) * feat(web): Replace bespoke navigation menu with shadcn navigation menu (#11117) Co-authored-by: Roo Code <roomote@roocode.com> --------- Co-authored-by: SannidhyaSah <sah_sannidhya@outlook.com> Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Hannes Rudolph <hrudolph@gmail.com> Co-authored-by: 0xMink <dennis@dennismink.com> Co-authored-by: Robert McIntyre <robertjmcintyre@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> Co-authored-by: Chris Estreich <cestreich@gmail.com>
1703 lines
55 KiB
TypeScript
1703 lines
55 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from "react"
|
|
import {
|
|
VSCodeCheckbox,
|
|
VSCodeRadioGroup,
|
|
VSCodeRadio,
|
|
VSCodeTextArea,
|
|
VSCodeLink,
|
|
VSCodeTextField,
|
|
} from "@vscode/webview-ui-toolkit/react"
|
|
import { Trans } from "react-i18next"
|
|
import { ChevronDown, X, Upload, Download } from "lucide-react"
|
|
|
|
import { ModeConfig, GroupEntry, PromptComponent, ToolGroup, modeConfigSchema } from "@roo-code/types"
|
|
|
|
import {
|
|
Mode,
|
|
getRoleDefinition,
|
|
getWhenToUse,
|
|
getDescription,
|
|
getCustomInstructions,
|
|
getAllModes,
|
|
findModeBySlug as findCustomModeBySlug,
|
|
defaultModeSlug,
|
|
} from "@roo/modes"
|
|
import { TOOL_GROUPS } from "@roo/tools"
|
|
|
|
import { vscode } from "@src/utils/vscode"
|
|
import { buildDocLink } from "@src/utils/docLinks"
|
|
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
|
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|
import { Section } from "@src/components/settings/Section"
|
|
import {
|
|
Button,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Command,
|
|
CommandInput,
|
|
CommandList,
|
|
CommandEmpty,
|
|
CommandItem,
|
|
CommandGroup,
|
|
Input,
|
|
StandardTooltip,
|
|
} from "@src/components/ui"
|
|
import { DeleteModeDialog } from "@src/components/modes/DeleteModeDialog"
|
|
import { useEscapeKey } from "@src/hooks/useEscapeKey"
|
|
|
|
// Get all available groups that should show in prompts view
|
|
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
|
|
|
|
type ModeSource = "global" | "project"
|
|
|
|
type ImportModeResult = { type: "importModeResult"; success: boolean; slug?: string; error?: string }
|
|
|
|
// Helper to get group name regardless of format
|
|
function getGroupName(group: GroupEntry): ToolGroup {
|
|
return Array.isArray(group) ? group[0] : group
|
|
}
|
|
|
|
const ModesView = () => {
|
|
const { t } = useAppTranslation()
|
|
|
|
const {
|
|
customModePrompts,
|
|
listApiConfigMeta,
|
|
currentApiConfigName,
|
|
mode,
|
|
customInstructions,
|
|
setCustomInstructions,
|
|
customModes,
|
|
} = useExtensionState()
|
|
|
|
// Use a local state to track the visually active mode
|
|
// This prevents flickering when switching modes rapidly by:
|
|
// 1. Updating the UI immediately when a mode is clicked
|
|
// 2. Not syncing with the backend mode state (which would cause flickering)
|
|
// 3. Still sending the mode change to the backend for persistence
|
|
const [visualMode, setVisualMode] = useState(mode)
|
|
|
|
// Build modes fresh each render so search reflects inline rename updates immediately
|
|
const modes = getAllModes(customModes)
|
|
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
|
const [selectedPromptContent, setSelectedPromptContent] = useState("")
|
|
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
|
|
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
|
|
const [showConfigMenu, setShowConfigMenu] = useState(false)
|
|
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
|
const [isExporting, setIsExporting] = useState(false)
|
|
const [isImporting, setIsImporting] = useState(false)
|
|
const [showImportDialog, setShowImportDialog] = useState(false)
|
|
const [importLevel, setImportLevel] = useState<"global" | "project">("project")
|
|
const [hasRulesToExport, setHasRulesToExport] = useState<Record<string, boolean>>({})
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
const [modeToDelete, setModeToDelete] = useState<{
|
|
slug: string
|
|
name: string
|
|
source?: string
|
|
rulesFolderPath?: string
|
|
} | null>(null)
|
|
|
|
// State for mode selection popover and search
|
|
const [open, setOpen] = useState(false)
|
|
const [searchValue, setSearchValue] = useState("")
|
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
// removed unused local name state (replaced by inline rename UX)
|
|
|
|
// Inline rename state for the mode dropdown row
|
|
const [isRenamingMode, setIsRenamingMode] = useState(false)
|
|
const [renameInputValue, setRenameInputValue] = useState("")
|
|
const renameInputRef = useRef<any>(null)
|
|
|
|
// Optimistic rename map so search reflects new names immediately
|
|
const [localRenames, setLocalRenames] = useState<Record<string, string>>({})
|
|
// Display list that overlays optimistic names
|
|
const displayModes = (modes || []).map((m) => (localRenames[m.slug] ? { ...m, name: localRenames[m.slug] } : m))
|
|
|
|
// Direct update functions
|
|
const updateAgentPrompt = useCallback(
|
|
(mode: Mode, promptData: PromptComponent) => {
|
|
const existingPrompt = customModePrompts?.[mode] as PromptComponent
|
|
const updatedPrompt = { ...existingPrompt, ...promptData }
|
|
|
|
// Only include properties that differ from defaults
|
|
if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) {
|
|
delete updatedPrompt.roleDefinition
|
|
}
|
|
if (updatedPrompt.description === getDescription(mode)) {
|
|
delete updatedPrompt.description
|
|
}
|
|
if (updatedPrompt.whenToUse === getWhenToUse(mode)) {
|
|
delete updatedPrompt.whenToUse
|
|
}
|
|
|
|
vscode.postMessage({
|
|
type: "updatePrompt",
|
|
promptMode: mode,
|
|
customPrompt: updatedPrompt,
|
|
})
|
|
},
|
|
[customModePrompts],
|
|
)
|
|
|
|
const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => {
|
|
const source = modeConfig.source || "global"
|
|
|
|
vscode.postMessage({
|
|
type: "updateCustomMode",
|
|
slug,
|
|
modeConfig: {
|
|
...modeConfig,
|
|
source, // Ensure source is set
|
|
},
|
|
})
|
|
}, [])
|
|
|
|
// Helper function to find a mode by slug
|
|
const findModeBySlug = useCallback(
|
|
(searchSlug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined => {
|
|
return findCustomModeBySlug(searchSlug, modes)
|
|
},
|
|
[],
|
|
)
|
|
|
|
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 === visualMode) return // Prevent unnecessary updates
|
|
|
|
// Immediately update visual state for instant feedback
|
|
setVisualMode(modeConfig.slug)
|
|
|
|
// Then send the mode change message to the backend
|
|
switchMode(modeConfig.slug)
|
|
|
|
// Exit tools edit mode when switching modes
|
|
setIsToolsEditMode(false)
|
|
},
|
|
[visualMode, switchMode],
|
|
)
|
|
|
|
// Refs to track latest state/functions for message handler (which has no dependencies)
|
|
const handleModeSwitchRef = useRef(handleModeSwitch)
|
|
const customModesRef = useRef(customModes)
|
|
const switchModeRef = useRef(switchMode)
|
|
|
|
// Update refs when dependencies change
|
|
useEffect(() => {
|
|
handleModeSwitchRef.current = handleModeSwitch
|
|
}, [handleModeSwitch])
|
|
|
|
useEffect(() => {
|
|
customModesRef.current = customModes
|
|
}, [customModes])
|
|
|
|
useEffect(() => {
|
|
switchModeRef.current = switchMode
|
|
}, [switchMode])
|
|
|
|
// Sync visualMode with backend mode changes to prevent desync
|
|
useEffect(() => {
|
|
setVisualMode(mode)
|
|
}, [mode])
|
|
|
|
// Handler for popover open state change
|
|
const onOpenChange = useCallback((open: boolean) => {
|
|
setOpen(open)
|
|
// Reset search when closing the popover
|
|
if (!open) {
|
|
setTimeout(() => setSearchValue(""), 100)
|
|
}
|
|
}, [])
|
|
|
|
// Use the shared ESC key handler hook
|
|
useEscapeKey(open, () => setOpen(false))
|
|
|
|
// Handler for clearing search input
|
|
const onClearSearch = useCallback(() => {
|
|
setSearchValue("")
|
|
searchInputRef.current?.focus()
|
|
}, [])
|
|
|
|
// Focus rename input when entering rename mode
|
|
useEffect(() => {
|
|
if (isRenamingMode) {
|
|
const id = setTimeout(() => renameInputRef.current?.focus(), 0)
|
|
return () => clearTimeout(id)
|
|
}
|
|
}, [isRenamingMode])
|
|
|
|
const handleStartRenameMode = useCallback(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
setIsRenamingMode(true)
|
|
setRenameInputValue(customMode.name)
|
|
}
|
|
}, [visualMode, customModes, findModeBySlug])
|
|
|
|
const handleCancelRenameMode = useCallback(() => {
|
|
setIsRenamingMode(false)
|
|
setRenameInputValue("")
|
|
}, [])
|
|
|
|
const handleSaveRenameMode = useCallback(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
const trimmed = renameInputValue.trim()
|
|
if (!customMode || !trimmed) {
|
|
setIsRenamingMode(false)
|
|
return
|
|
}
|
|
// Prevent duplicate names against other modes
|
|
const nameTaken = modes.some(
|
|
(m) => m.name.toLowerCase() === trimmed.toLowerCase() && m.slug !== customMode.slug,
|
|
)
|
|
if (nameTaken) {
|
|
// simple guard: do nothing if taken
|
|
return
|
|
}
|
|
updateCustomMode(visualMode, {
|
|
...customMode,
|
|
name: trimmed,
|
|
source: customMode.source || "global",
|
|
})
|
|
// Optimistically reflect rename in UI/search immediately
|
|
setLocalRenames((prev) => ({ ...prev, [visualMode]: trimmed }))
|
|
setIsRenamingMode(false)
|
|
}, [visualMode, customModes, renameInputValue, modes, updateCustomMode, findModeBySlug])
|
|
|
|
// Helper function to get current mode's config
|
|
const getCurrentMode = useCallback((): ModeConfig | undefined => {
|
|
const findMode = (m: ModeConfig): boolean => m.slug === visualMode
|
|
return customModes?.find(findMode) || modes.find(findMode)
|
|
}, [visualMode, customModes, modes])
|
|
|
|
// Check if the current mode has rules to export
|
|
const checkRulesDirectory = useCallback((slug: string) => {
|
|
vscode.postMessage({
|
|
type: "checkRulesDirectory",
|
|
slug: slug,
|
|
})
|
|
}, [])
|
|
|
|
// Check rules directory when mode changes
|
|
useEffect(() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug && hasRulesToExport[currentMode.slug] === undefined) {
|
|
checkRulesDirectory(currentMode.slug)
|
|
}
|
|
}, [getCurrentMode, checkRulesDirectory, hasRulesToExport])
|
|
|
|
// State for create mode dialog
|
|
const [newModeName, setNewModeName] = useState("")
|
|
const [newModeSlug, setNewModeSlug] = useState("")
|
|
const [newModeDescription, setNewModeDescription] = useState("")
|
|
const [newModeRoleDefinition, setNewModeRoleDefinition] = useState("")
|
|
const [newModeWhenToUse, setNewModeWhenToUse] = useState("")
|
|
const [newModeCustomInstructions, setNewModeCustomInstructions] = useState("")
|
|
const [newModeGroups, setNewModeGroups] = useState<GroupEntry[]>(availableGroups)
|
|
const [newModeSource, setNewModeSource] = useState<ModeSource>("global")
|
|
|
|
// Field-specific error states
|
|
const [nameError, setNameError] = useState<string>("")
|
|
const [slugError, setSlugError] = useState<string>("")
|
|
const [descriptionError, setDescriptionError] = useState<string>("")
|
|
const [roleDefinitionError, setRoleDefinitionError] = useState<string>("")
|
|
const [groupsError, setGroupsError] = useState<string>("")
|
|
|
|
// Helper to reset form state
|
|
const resetFormState = useCallback(() => {
|
|
// Reset form fields
|
|
setNewModeName("")
|
|
setNewModeSlug("")
|
|
setNewModeDescription("")
|
|
setNewModeGroups(availableGroups)
|
|
setNewModeRoleDefinition("")
|
|
setNewModeWhenToUse("")
|
|
setNewModeCustomInstructions("")
|
|
setNewModeSource("global")
|
|
// Reset error states
|
|
setNameError("")
|
|
setSlugError("")
|
|
setDescriptionError("")
|
|
setRoleDefinitionError("")
|
|
setGroupsError("")
|
|
}, [])
|
|
|
|
// Reset form fields when dialog opens
|
|
useEffect(() => {
|
|
if (isCreateModeDialogOpen) {
|
|
resetFormState()
|
|
}
|
|
}, [isCreateModeDialogOpen, resetFormState])
|
|
|
|
// Ensure import dialog defaults to "project" each open
|
|
useEffect(() => {
|
|
if (showImportDialog) {
|
|
setImportLevel("project")
|
|
}
|
|
}, [showImportDialog])
|
|
|
|
// 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(() => {
|
|
// Clear previous errors
|
|
setNameError("")
|
|
setSlugError("")
|
|
setDescriptionError("")
|
|
setRoleDefinitionError("")
|
|
setGroupsError("")
|
|
|
|
const source = newModeSource
|
|
const newMode: ModeConfig = {
|
|
slug: newModeSlug,
|
|
name: newModeName,
|
|
description: newModeDescription.trim() || undefined,
|
|
roleDefinition: newModeRoleDefinition.trim(),
|
|
whenToUse: newModeWhenToUse.trim() || undefined,
|
|
customInstructions: newModeCustomInstructions.trim() || undefined,
|
|
groups: newModeGroups,
|
|
source,
|
|
}
|
|
|
|
// Validate the mode against the schema
|
|
const result = modeConfigSchema.safeParse(newMode)
|
|
|
|
if (!result.success) {
|
|
// Map Zod errors to specific fields
|
|
result.error.errors.forEach((error) => {
|
|
const field = error.path[0] as string
|
|
const message = error.message
|
|
|
|
switch (field) {
|
|
case "name":
|
|
setNameError(message)
|
|
break
|
|
case "slug":
|
|
setSlugError(message)
|
|
break
|
|
case "description":
|
|
setDescriptionError(message)
|
|
break
|
|
case "roleDefinition":
|
|
setRoleDefinitionError(message)
|
|
break
|
|
case "groups":
|
|
setGroupsError(message)
|
|
break
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
updateCustomMode(newModeSlug, newMode)
|
|
// Immediately select the newly created mode in the UI
|
|
setVisualMode(newModeSlug)
|
|
switchMode(newModeSlug)
|
|
setIsCreateModeDialogOpen(false)
|
|
resetFormState()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
newModeName,
|
|
newModeSlug,
|
|
newModeDescription,
|
|
newModeRoleDefinition,
|
|
newModeWhenToUse, // Add whenToUse dependency
|
|
newModeCustomInstructions,
|
|
newModeGroups,
|
|
newModeSource,
|
|
updateCustomMode,
|
|
])
|
|
|
|
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: GroupEntry[]
|
|
if (checked) {
|
|
newGroups = [...oldGroups, group]
|
|
} else {
|
|
newGroups = oldGroups.filter((g) => getGroupName(g) !== group)
|
|
}
|
|
if (customMode) {
|
|
const source = customMode.source || "global"
|
|
|
|
updateCustomMode(customMode.slug, {
|
|
...customMode,
|
|
groups: newGroups,
|
|
source,
|
|
})
|
|
}
|
|
},
|
|
[updateCustomMode],
|
|
)
|
|
|
|
// Handle clicks outside the config menu
|
|
useEffect(() => {
|
|
const handleClickOutside = () => {
|
|
if (showConfigMenu) {
|
|
setShowConfigMenu(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener("click", handleClickOutside)
|
|
return () => document.removeEventListener("click", handleClickOutside)
|
|
}, [showConfigMenu])
|
|
|
|
// Use a ref to store the current modeToDelete value
|
|
const modeToDeleteRef = useRef(modeToDelete)
|
|
|
|
// Update the ref whenever modeToDelete changes
|
|
useEffect(() => {
|
|
modeToDeleteRef.current = modeToDelete
|
|
}, [modeToDelete])
|
|
|
|
useEffect(() => {
|
|
const handler = (event: MessageEvent) => {
|
|
const message = event.data
|
|
if (message.type === "systemPrompt") {
|
|
if (message.text) {
|
|
setSelectedPromptContent(message.text)
|
|
setSelectedPromptTitle(`System Prompt (${message.mode} mode)`)
|
|
setIsDialogOpen(true)
|
|
}
|
|
} else if (message.type === "exportModeResult") {
|
|
setIsExporting(false)
|
|
|
|
if (!message.success) {
|
|
// Show error message
|
|
console.error("Failed to export mode:", message.error)
|
|
}
|
|
} else if (message.type === "importModeResult") {
|
|
setIsImporting(false)
|
|
setShowImportDialog(false)
|
|
|
|
if (message.success) {
|
|
const { slug } = message as ImportModeResult
|
|
if (slug) {
|
|
// Try switching using the freshest mode list available
|
|
const all = getAllModes(customModesRef.current)
|
|
const importedMode = all.find((m) => m.slug === slug)
|
|
if (importedMode) {
|
|
handleModeSwitchRef.current(importedMode)
|
|
} else {
|
|
// Fallback: slug not yet in state (race condition) - select default mode
|
|
setVisualMode(defaultModeSlug)
|
|
switchModeRef.current?.(defaultModeSlug)
|
|
}
|
|
}
|
|
} else {
|
|
// Only log error if it's not a cancellation
|
|
if (message.error !== "cancelled") {
|
|
console.error("Failed to import mode:", message.error)
|
|
}
|
|
}
|
|
// Note: Auto-select after import will be handled by PR #9003
|
|
} else if (message.type === "checkRulesDirectoryResult") {
|
|
setHasRulesToExport((prev) => ({
|
|
...prev,
|
|
[message.slug]: message.hasContent,
|
|
}))
|
|
} else if (message.type === "deleteCustomModeCheck") {
|
|
// Handle the check response
|
|
// Use the ref to get the current modeToDelete value
|
|
const currentModeToDelete = modeToDeleteRef.current
|
|
if (message.slug && currentModeToDelete && currentModeToDelete.slug === message.slug) {
|
|
setModeToDelete({
|
|
...currentModeToDelete,
|
|
rulesFolderPath: message.rulesFolderPath,
|
|
})
|
|
setShowDeleteConfirm(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener("message", handler)
|
|
return () => window.removeEventListener("message", handler)
|
|
}, [checkRulesDirectory, switchMode])
|
|
|
|
const handleAgentReset = (
|
|
modeSlug: string,
|
|
type: "roleDefinition" | "description" | "whenToUse" | "customInstructions",
|
|
) => {
|
|
// Only reset for built-in modes
|
|
const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent
|
|
const updatedPrompt = { ...existingPrompt }
|
|
delete updatedPrompt[type] // Remove the field entirely to ensure it reloads from defaults
|
|
|
|
vscode.postMessage({
|
|
type: "updatePrompt",
|
|
promptMode: modeSlug,
|
|
customPrompt: updatedPrompt,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Section>
|
|
<div>
|
|
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
|
|
<h3 className="text-[1.25em] font-semibold text-vscode-foreground mt-4 mb-2">
|
|
{t("prompts:modes.title")}
|
|
</h3>
|
|
<div className="flex gap-2">
|
|
<div className="relative inline-block">
|
|
<StandardTooltip content={t("prompts:modes.editModesConfig")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="flex"
|
|
onClick={(e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
setShowConfigMenu((prev) => !prev)
|
|
}}
|
|
onBlur={() => {
|
|
// Add slight delay to allow menu item clicks to register
|
|
setTimeout(() => setShowConfigMenu(false), 200)
|
|
}}>
|
|
<span className="codicon codicon-json"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
{showConfigMenu && (
|
|
<div
|
|
onClick={(e) => e.stopPropagation()}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
className="absolute top-full right-0 w-[200px] mt-1 bg-vscode-editor-background border border-vscode-input-border rounded shadow-md z-[1000]">
|
|
<div
|
|
className="p-2 cursor-pointer text-vscode-foreground text-sm"
|
|
onMouseDown={(e) => {
|
|
e.preventDefault() // Prevent blur
|
|
vscode.postMessage({
|
|
type: "openCustomModesSettings",
|
|
})
|
|
setShowConfigMenu(false)
|
|
}}
|
|
onClick={(e) => e.preventDefault()}>
|
|
{t("prompts:modes.editGlobalModes")}
|
|
</div>
|
|
<div
|
|
className="p-2 cursor-pointer text-vscode-foreground text-sm border-t border-vscode-input-border"
|
|
onMouseDown={(e) => {
|
|
e.preventDefault() // Prevent blur
|
|
vscode.postMessage({
|
|
type: "openFile",
|
|
text: "./.roomodes",
|
|
values: {
|
|
create: true,
|
|
content: JSON.stringify({ customModes: [] }, null, 2),
|
|
},
|
|
})
|
|
setShowConfigMenu(false)
|
|
}}
|
|
onClick={(e) => e.preventDefault()}>
|
|
{t("prompts:modes.editProjectModes")}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<StandardTooltip content={t("chat:modeSelector.marketplace")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
window.postMessage(
|
|
{
|
|
type: "action",
|
|
action: "marketplaceButtonClicked",
|
|
values: { marketplaceTab: "mode" },
|
|
},
|
|
"*",
|
|
)
|
|
}}>
|
|
<span className="codicon codicon-extensions"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
|
|
<StandardTooltip content={t("prompts:modes.importMode")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setShowImportDialog(true)}
|
|
disabled={isImporting}
|
|
title={t("prompts:modes.importMode")}
|
|
data-testid="import-mode-toolbar-button">
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm text-vscode-descriptionForeground mb-3">
|
|
<Trans i18nKey="prompts:modes.createModeHelpText">
|
|
<VSCodeLink
|
|
href={buildDocLink("basic-usage/using-modes", "prompts_view_modes")}
|
|
style={{ display: "inline" }}
|
|
aria-label="Learn about using modes"></VSCodeLink>
|
|
<VSCodeLink
|
|
href={buildDocLink("features/custom-modes", "prompts_view_modes")}
|
|
style={{ display: "inline" }}
|
|
aria-label="Learn about customizing modes"></VSCodeLink>
|
|
</Trans>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 mb-3">
|
|
{isRenamingMode ? (
|
|
<>
|
|
<VSCodeTextField
|
|
ref={renameInputRef}
|
|
value={renameInputValue}
|
|
onInput={(e: unknown) => {
|
|
const target = e as { target: { value: string } }
|
|
setRenameInputValue(target.target.value)
|
|
}}
|
|
className="grow"
|
|
placeholder={t("prompts:createModeDialog.name.placeholder")}
|
|
/>
|
|
<StandardTooltip content={t("settings:common.save")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
disabled={!renameInputValue.trim()}
|
|
onClick={handleSaveRenameMode}
|
|
data-testid="save-mode-rename-button">
|
|
<span className="codicon codicon-check" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
<StandardTooltip content={t("settings:common.cancel")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleCancelRenameMode}
|
|
data-testid="cancel-mode-rename-button">
|
|
<span className="codicon codicon-close" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Popover open={open} onOpenChange={onOpenChange}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="combobox"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className="justify-between grow"
|
|
data-testid="mode-select-trigger">
|
|
<div className="truncate">
|
|
{localRenames[visualMode] ??
|
|
getCurrentMode()?.name ??
|
|
t("prompts:modes.selectMode")}
|
|
</div>
|
|
<ChevronDown className="opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
|
|
<Command>
|
|
<div className="relative">
|
|
<CommandInput
|
|
ref={searchInputRef}
|
|
value={searchValue}
|
|
onValueChange={setSearchValue}
|
|
placeholder={t("prompts:modes.selectMode")}
|
|
className="h-9 mr-4"
|
|
data-testid="mode-search-input"
|
|
/>
|
|
{searchValue.length > 0 && (
|
|
<div className="absolute right-2 top-0 bottom-0 flex items-center justify-center">
|
|
<X
|
|
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
|
|
onClick={onClearSearch}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<CommandList>
|
|
<CommandEmpty>
|
|
{searchValue && (
|
|
<div className="py-2 px-1 text-sm">
|
|
{t("prompts:modes.noMatchFound")}
|
|
</div>
|
|
)}
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{displayModes
|
|
.filter((modeConfig) =>
|
|
searchValue
|
|
? modeConfig.name
|
|
.toLowerCase()
|
|
.includes(searchValue.toLowerCase())
|
|
: true,
|
|
)
|
|
.map((modeConfig) => (
|
|
<CommandItem
|
|
key={modeConfig.slug}
|
|
value={`${modeConfig.name} ${modeConfig.slug}`}
|
|
onSelect={() => {
|
|
handleModeSwitch(modeConfig)
|
|
setOpen(false)
|
|
}}
|
|
data-testid={`mode-option-${modeConfig.slug}`}>
|
|
<div className="flex items-center justify-between w-full">
|
|
<span
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
flex: 2,
|
|
minWidth: 0,
|
|
}}>
|
|
{modeConfig.name}
|
|
</span>
|
|
<span
|
|
className="text-foreground"
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
direction: "rtl",
|
|
textAlign: "right",
|
|
flex: 1,
|
|
minWidth: 0,
|
|
marginLeft: "0.5em",
|
|
}}>
|
|
{modeConfig.slug}
|
|
</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
{/* New mode (+) moved here from the top bar */}
|
|
<StandardTooltip content={t("prompts:modes.createNewMode")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={openCreateModeDialog}
|
|
data-testid="add-mode-button">
|
|
<span className="codicon codicon-add" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
|
|
{/* Edit (rename) mode - only enabled for custom modes */}
|
|
<StandardTooltip content={t("settings:providers.renameProfile")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleStartRenameMode}
|
|
data-testid="rename-mode-button"
|
|
disabled={!findModeBySlug(visualMode, customModes)}>
|
|
<span className="codicon codicon-edit" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
|
|
{/* Delete mode - disabled for built-in modes */}
|
|
<StandardTooltip content={t("prompts:createModeDialog.deleteMode")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
setModeToDelete({
|
|
slug: customMode.slug,
|
|
name: customMode.name,
|
|
source: customMode.source || "global",
|
|
})
|
|
vscode.postMessage({
|
|
type: "deleteCustomMode",
|
|
slug: customMode.slug,
|
|
checkOnly: true,
|
|
})
|
|
}
|
|
}}
|
|
data-testid="delete-mode-button"
|
|
disabled={!findModeBySlug(visualMode, customModes)}>
|
|
<span className="codicon codicon-trash" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
|
|
{/* Export mode (kept here to the right of the dropdown) */}
|
|
<StandardTooltip content={t("prompts:exportMode.title")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug && !isExporting) {
|
|
setIsExporting(true)
|
|
vscode.postMessage({
|
|
type: "exportMode",
|
|
slug: currentMode.slug,
|
|
})
|
|
}
|
|
}}
|
|
disabled={isExporting}
|
|
title={t("prompts:exportMode.title")}
|
|
data-testid="export-mode-toolbar-button">
|
|
<Upload className="h-4 w-4" />
|
|
</Button>
|
|
</StandardTooltip>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* API Configuration - Moved Here */}
|
|
<div className="mb-3">
|
|
<div className="font-bold mb-1">{t("prompts:apiConfiguration.title")}</div>
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:apiConfiguration.select")}
|
|
</div>
|
|
<div className="mb-2">
|
|
<Select
|
|
value={currentApiConfigName}
|
|
onValueChange={(value) => {
|
|
vscode.postMessage({
|
|
type: "loadApiConfiguration",
|
|
text: value,
|
|
})
|
|
}}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder={t("settings:common.select")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(listApiConfigMeta || []).map((config) => (
|
|
<SelectItem key={config.id} value={config.name}>
|
|
{config.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Role Definition section */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<div className="font-bold">{t("prompts:roleDefinition.title")}</div>
|
|
{!findModeBySlug(visualMode, customModes) && (
|
|
<StandardTooltip content={t("prompts:roleDefinition.resetToDefault")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug) {
|
|
handleAgentReset(currentMode.slug, "roleDefinition")
|
|
}
|
|
}}
|
|
data-testid="role-definition-reset">
|
|
<span className="codicon codicon-discard"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:roleDefinition.description")}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
|
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(visualMode)
|
|
})()}
|
|
onChange={(e) => {
|
|
const value =
|
|
(e as unknown as CustomEvent)?.detail?.target?.value ??
|
|
((e as any).target as HTMLTextAreaElement).value
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
// For custom modes, update the JSON file
|
|
updateCustomMode(visualMode, {
|
|
...customMode,
|
|
roleDefinition: value.trim() || "",
|
|
source: customMode.source || "global",
|
|
})
|
|
} else {
|
|
// For built-in modes, update the prompts
|
|
updateAgentPrompt(visualMode, {
|
|
roleDefinition: value.trim() || undefined,
|
|
})
|
|
}
|
|
}}
|
|
className="w-full"
|
|
rows={5}
|
|
data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Description section */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<div className="font-bold">{t("prompts:description.title")}</div>
|
|
{!findModeBySlug(visualMode, customModes) && (
|
|
<StandardTooltip content={t("prompts:description.resetToDefault")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug) {
|
|
handleAgentReset(currentMode.slug, "description")
|
|
}
|
|
}}
|
|
data-testid="description-reset">
|
|
<span className="codicon codicon-discard"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:description.description")}
|
|
</div>
|
|
<VSCodeTextField
|
|
value={(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
|
return customMode?.description ?? prompt?.description ?? getDescription(visualMode)
|
|
})()}
|
|
onChange={(e) => {
|
|
const value =
|
|
(e as unknown as CustomEvent)?.detail?.target?.value ??
|
|
((e as any).target as HTMLTextAreaElement).value
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
// For custom modes, update the JSON file
|
|
updateCustomMode(visualMode, {
|
|
...customMode,
|
|
description: value.trim() || undefined,
|
|
source: customMode.source || "global",
|
|
})
|
|
} else {
|
|
// For built-in modes, update the prompts
|
|
updateAgentPrompt(visualMode, {
|
|
description: value.trim() || undefined,
|
|
})
|
|
}
|
|
}}
|
|
className="w-full"
|
|
data-testid={`${getCurrentMode()?.slug || "code"}-description-textfield`}
|
|
/>
|
|
</div>
|
|
|
|
{/* When to Use section */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<div className="font-bold">{t("prompts:whenToUse.title")}</div>
|
|
{!findModeBySlug(visualMode, customModes) && (
|
|
<StandardTooltip content={t("prompts:whenToUse.resetToDefault")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug) {
|
|
handleAgentReset(currentMode.slug, "whenToUse")
|
|
}
|
|
}}
|
|
data-testid="when-to-use-reset">
|
|
<span className="codicon codicon-discard"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:whenToUse.description")}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
|
return customMode?.whenToUse ?? prompt?.whenToUse ?? getWhenToUse(visualMode)
|
|
})()}
|
|
onChange={(e) => {
|
|
const value =
|
|
(e as unknown as CustomEvent)?.detail?.target?.value ??
|
|
((e as any).target as HTMLTextAreaElement).value
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
// For custom modes, update the JSON file
|
|
updateCustomMode(visualMode, {
|
|
...customMode,
|
|
whenToUse: value.trim() || undefined,
|
|
source: customMode.source || "global",
|
|
})
|
|
} else {
|
|
// For built-in modes, update the prompts
|
|
updateAgentPrompt(visualMode, {
|
|
whenToUse: value.trim() || undefined,
|
|
})
|
|
}
|
|
}}
|
|
className="w-full"
|
|
rows={4}
|
|
data-testid={`${getCurrentMode()?.slug || "code"}-when-to-use-textarea`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Mode settings */}
|
|
<>
|
|
{/* Show tools for all modes */}
|
|
<div className="mb-4">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<div className="font-bold">{t("prompts:tools.title")}</div>
|
|
{findModeBySlug(visualMode, customModes) && (
|
|
<StandardTooltip
|
|
content={
|
|
isToolsEditMode ? t("prompts:tools.doneEditing") : t("prompts:tools.editTools")
|
|
}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsToolsEditMode(!isToolsEditMode)}>
|
|
<span
|
|
className={`codicon codicon-${isToolsEditMode ? "check" : "edit"}`}></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
)}
|
|
</div>
|
|
{!findModeBySlug(visualMode, customModes) && (
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:tools.builtInModesText")}
|
|
</div>
|
|
)}
|
|
{isToolsEditMode && findModeBySlug(visualMode, customModes) ? (
|
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
|
|
{availableGroups.map((group) => {
|
|
const currentMode = getCurrentMode()
|
|
const isCustomMode = findModeBySlug(visualMode, customModes)
|
|
const customMode = isCustomMode
|
|
const isGroupEnabled = isCustomMode
|
|
? customMode?.groups?.some((g) => getGroupName(g) === group)
|
|
: currentMode?.groups?.some((g) => getGroupName(g) === group)
|
|
|
|
return (
|
|
<VSCodeCheckbox
|
|
key={group}
|
|
checked={isGroupEnabled}
|
|
onChange={handleGroupChange(group, Boolean(isCustomMode), customMode)}
|
|
disabled={!isCustomMode}>
|
|
{t(`prompts:tools.toolNames.${group}`)}
|
|
{group === "edit" && (
|
|
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
|
|
{t("prompts:tools.allowedFiles")}{" "}
|
|
{(() => {
|
|
const currentMode = getCurrentMode()
|
|
const editGroup = currentMode?.groups?.find(
|
|
(g) =>
|
|
Array.isArray(g) && g[0] === "edit" && g[1]?.fileRegex,
|
|
)
|
|
if (!Array.isArray(editGroup)) return t("prompts:allFiles")
|
|
return editGroup[1].description || `/${editGroup[1].fileRegex}/`
|
|
})()}
|
|
</div>
|
|
)}
|
|
</VSCodeCheckbox>
|
|
)
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-vscode-foreground mb-2 leading-relaxed">
|
|
{(() => {
|
|
const currentMode = getCurrentMode()
|
|
const enabledGroups = currentMode?.groups || []
|
|
|
|
// If there are no enabled groups, display translated "None"
|
|
if (enabledGroups.length === 0) {
|
|
return t("prompts:tools.noTools")
|
|
}
|
|
|
|
return enabledGroups
|
|
.map((group) => {
|
|
const groupName = getGroupName(group)
|
|
const displayName = t(`prompts:tools.toolNames.${groupName}`)
|
|
if (Array.isArray(group) && group[1]?.fileRegex) {
|
|
const description = group[1].description || `/${group[1].fileRegex}/`
|
|
return `${displayName} (${description})`
|
|
}
|
|
return displayName
|
|
})
|
|
.join(", ")
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
|
|
{/* Role definition for both built-in and custom modes */}
|
|
<div className="mb-2">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<div className="font-bold">{t("prompts:customInstructions.title")}</div>
|
|
{!findModeBySlug(visualMode, customModes) && (
|
|
<StandardTooltip content={t("prompts:customInstructions.resetToDefault")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode?.slug) {
|
|
handleAgentReset(currentMode.slug, "customInstructions")
|
|
}
|
|
}}
|
|
data-testid="custom-instructions-reset">
|
|
<span className="codicon codicon-discard"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
)}
|
|
</div>
|
|
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:customInstructions.description", {
|
|
modeName: getCurrentMode()?.name || "Code",
|
|
})}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={(() => {
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
const prompt = customModePrompts?.[visualMode] as PromptComponent
|
|
return (
|
|
customMode?.customInstructions ??
|
|
prompt?.customInstructions ??
|
|
getCustomInstructions(visualMode, customModes)
|
|
)
|
|
})()}
|
|
onChange={(e) => {
|
|
const value =
|
|
(e as unknown as CustomEvent)?.detail?.target?.value ??
|
|
((e as any).target as HTMLTextAreaElement).value
|
|
const customMode = findModeBySlug(visualMode, customModes)
|
|
if (customMode) {
|
|
// For custom modes, update the JSON file
|
|
updateCustomMode(visualMode, {
|
|
...customMode,
|
|
// Preserve empty string; only treat null/undefined as unset
|
|
customInstructions: value ?? undefined,
|
|
source: customMode.source || "global",
|
|
})
|
|
} else {
|
|
// For built-in modes, update the prompts
|
|
const existingPrompt = customModePrompts?.[visualMode] as PromptComponent
|
|
updateAgentPrompt(visualMode, {
|
|
...existingPrompt,
|
|
customInstructions: value.trim() || undefined,
|
|
})
|
|
}
|
|
}}
|
|
rows={10}
|
|
className="w-full"
|
|
data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`}
|
|
/>
|
|
<div className="text-xs text-vscode-descriptionForeground mt-1.5">
|
|
<Trans
|
|
i18nKey="prompts:customInstructions.loadFromFile"
|
|
values={{
|
|
mode: getCurrentMode()?.name || "Code",
|
|
slug: getCurrentMode()?.slug || "code",
|
|
}}
|
|
components={{
|
|
span: (
|
|
<span
|
|
className="text-vscode-textLink-foreground cursor-pointer underline"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (!currentMode) return
|
|
|
|
// Open or create an empty file
|
|
vscode.postMessage({
|
|
type: "openFile",
|
|
text: `./.roo/rules-${currentMode.slug}/rules.md`,
|
|
values: {
|
|
create: true,
|
|
content: "",
|
|
},
|
|
})
|
|
}}
|
|
/>
|
|
),
|
|
"0": (
|
|
<VSCodeLink
|
|
href={buildDocLink(
|
|
"features/custom-instructions#global-rules-directory",
|
|
"prompts_mode_specific_global_rules",
|
|
)}
|
|
style={{ display: "inline" }}
|
|
aria-label="Learn about global custom instructions for modes"
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pb-4 border-b border-vscode-input-border">
|
|
<div className="flex gap-2 mb-4">
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode) {
|
|
vscode.postMessage({
|
|
type: "getSystemPrompt",
|
|
mode: currentMode.slug,
|
|
})
|
|
}
|
|
}}
|
|
data-testid="preview-prompt-button">
|
|
{t("prompts:systemPrompt.preview")}
|
|
</Button>
|
|
<StandardTooltip content={t("prompts:systemPrompt.copy")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const currentMode = getCurrentMode()
|
|
if (currentMode) {
|
|
vscode.postMessage({
|
|
type: "copySystemPrompt",
|
|
mode: currentMode.slug,
|
|
})
|
|
}
|
|
}}
|
|
data-testid="copy-prompt-button">
|
|
<span className="codicon codicon-copy"></span>
|
|
</Button>
|
|
</StandardTooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pb-5">
|
|
<h3 className="text-vscode-foreground mb-3">{t("prompts:globalCustomInstructions.title")}</h3>
|
|
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
<Trans i18nKey="prompts:globalCustomInstructions.description">
|
|
<VSCodeLink
|
|
href={buildDocLink(
|
|
"features/custom-instructions#setting-up-global-rules",
|
|
"prompts_global_custom_instructions",
|
|
)}
|
|
style={{ display: "inline" }}
|
|
aria-label="Learn more about global custom instructions"></VSCodeLink>
|
|
</Trans>
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={customInstructions || ""}
|
|
onChange={(e) => {
|
|
const value =
|
|
(e as unknown as CustomEvent)?.detail?.target?.value ??
|
|
((e as any).target as HTMLTextAreaElement).value
|
|
setCustomInstructions(value ?? undefined)
|
|
vscode.postMessage({
|
|
type: "customInstructions",
|
|
text: value ?? undefined,
|
|
})
|
|
}}
|
|
rows={4}
|
|
className="w-full"
|
|
data-testid="global-custom-instructions-textarea"
|
|
/>
|
|
<div className="text-xs text-vscode-descriptionForeground mt-1.5">
|
|
<Trans
|
|
i18nKey="prompts:globalCustomInstructions.loadFromFile"
|
|
components={{
|
|
span: (
|
|
<span
|
|
className="text-vscode-textLink-foreground cursor-pointer underline"
|
|
onClick={() =>
|
|
vscode.postMessage({
|
|
type: "openFile",
|
|
text: "./.roo/rules/rules.md",
|
|
values: {
|
|
create: true,
|
|
content: "",
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
),
|
|
"0": (
|
|
<VSCodeLink
|
|
href={buildDocLink(
|
|
"features/custom-instructions#setting-up-global-rules",
|
|
"prompts_global_rules",
|
|
)}
|
|
style={{ display: "inline" }}
|
|
aria-label="Learn about setting up global custom instructions"
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{isCreateModeDialogOpen && (
|
|
<div className="fixed inset-0 flex justify-end bg-black/50 z-[1000]">
|
|
<div className="w-[calc(100vw-100px)] h-full bg-vscode-editor-background shadow-md flex flex-col relative">
|
|
<div className="flex-1 p-5 overflow-y-auto min-h-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsCreateModeDialogOpen(false)}
|
|
className="absolute top-5 right-5">
|
|
<span className="codicon codicon-close"></span>
|
|
</Button>
|
|
<h2 className="mb-4">{t("prompts:createModeDialog.title")}</h2>
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.name.label")}</div>
|
|
<Input
|
|
type="text"
|
|
value={newModeName}
|
|
onChange={(e) => {
|
|
handleNameChange(e.target.value)
|
|
}}
|
|
className="w-full"
|
|
/>
|
|
{nameError && (
|
|
<div className="text-xs text-vscode-errorForeground mt-1">{nameError}</div>
|
|
)}
|
|
</div>
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.slug.label")}</div>
|
|
<Input
|
|
type="text"
|
|
value={newModeSlug}
|
|
onChange={(e) => {
|
|
setNewModeSlug(e.target.value)
|
|
}}
|
|
className="w-full"
|
|
/>
|
|
<div className="text-xs text-vscode-descriptionForeground mt-1">
|
|
{t("prompts:createModeDialog.slug.description")}
|
|
</div>
|
|
{slugError && (
|
|
<div className="text-xs text-vscode-errorForeground mt-1">{slugError}</div>
|
|
)}
|
|
</div>
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.saveLocation.label")}</div>
|
|
<div className="text-sm text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:createModeDialog.saveLocation.description")}
|
|
</div>
|
|
<VSCodeRadioGroup
|
|
value={newModeSource}
|
|
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
|
const target = ((e as CustomEvent)?.detail?.target ||
|
|
(e.target as HTMLInputElement)) as HTMLInputElement
|
|
setNewModeSource(target.value as ModeSource)
|
|
}}>
|
|
<VSCodeRadio value="global">
|
|
{t("prompts:createModeDialog.saveLocation.global.label")}
|
|
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
|
|
{t("prompts:createModeDialog.saveLocation.global.description")}
|
|
</div>
|
|
</VSCodeRadio>
|
|
<VSCodeRadio value="project">
|
|
{t("prompts:createModeDialog.saveLocation.project.label")}
|
|
<div className="text-xs text-vscode-descriptionForeground mt-0.5">
|
|
{t("prompts:createModeDialog.saveLocation.project.description")}
|
|
</div>
|
|
</VSCodeRadio>
|
|
</VSCodeRadioGroup>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: "16px" }}>
|
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>
|
|
{t("prompts:createModeDialog.roleDefinition.label")}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: "13px",
|
|
color: "var(--vscode-descriptionForeground)",
|
|
marginBottom: "8px",
|
|
}}>
|
|
{t("prompts:createModeDialog.roleDefinition.description")}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={newModeRoleDefinition}
|
|
onChange={(e) => {
|
|
setNewModeRoleDefinition((e.target as HTMLTextAreaElement).value)
|
|
}}
|
|
rows={4}
|
|
className="w-full"
|
|
/>
|
|
{roleDefinitionError && (
|
|
<div className="text-xs text-vscode-errorForeground mt-1">
|
|
{roleDefinitionError}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.description.label")}</div>
|
|
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:createModeDialog.description.description")}
|
|
</div>
|
|
<VSCodeTextField
|
|
value={newModeDescription}
|
|
onChange={(e) => {
|
|
setNewModeDescription((e.target as HTMLInputElement).value)
|
|
}}
|
|
className="w-full"
|
|
/>
|
|
{descriptionError && (
|
|
<div className="text-xs text-vscode-errorForeground mt-1">{descriptionError}</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.whenToUse.label")}</div>
|
|
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:createModeDialog.whenToUse.description")}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={newModeWhenToUse}
|
|
onChange={(e) => {
|
|
setNewModeWhenToUse((e.target as HTMLTextAreaElement).value)
|
|
}}
|
|
rows={3}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">{t("prompts:createModeDialog.tools.label")}</div>
|
|
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:createModeDialog.tools.description")}
|
|
</div>
|
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
|
|
{availableGroups.map((group) => (
|
|
<VSCodeCheckbox
|
|
key={group}
|
|
checked={newModeGroups.some((g) => getGroupName(g) === 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) => getGroupName(g) !== group),
|
|
)
|
|
}
|
|
}}>
|
|
{t(`prompts:tools.toolNames.${group}`)}
|
|
</VSCodeCheckbox>
|
|
))}
|
|
</div>
|
|
{groupsError && (
|
|
<div className="text-xs text-vscode-errorForeground mt-1">{groupsError}</div>
|
|
)}
|
|
</div>
|
|
<div className="mb-4">
|
|
<div className="font-bold mb-1">
|
|
{t("prompts:createModeDialog.customInstructions.label")}
|
|
</div>
|
|
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
|
|
{t("prompts:createModeDialog.customInstructions.description")}
|
|
</div>
|
|
<VSCodeTextArea
|
|
resize="vertical"
|
|
value={newModeCustomInstructions}
|
|
onChange={(e) => {
|
|
setNewModeCustomInstructions((e.target as HTMLTextAreaElement).value)
|
|
}}
|
|
rows={4}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end p-3 px-5 gap-2 border-t border-vscode-editor-lineHighlightBorder bg-vscode-editor-background">
|
|
<Button variant="secondary" onClick={() => setIsCreateModeDialogOpen(false)}>
|
|
{t("prompts:createModeDialog.buttons.cancel")}
|
|
</Button>
|
|
<Button variant="primary" onClick={handleCreateMode}>
|
|
{t("prompts:createModeDialog.buttons.create")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isDialogOpen && (
|
|
<div className="fixed inset-0 flex justify-end bg-black/50 z-[1000]">
|
|
<div className="w-[calc(100vw-100px)] h-full bg-vscode-editor-background shadow-md flex flex-col relative">
|
|
<div className="flex-1 p-5 overflow-y-auto min-h-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
className="absolute top-5 right-5">
|
|
<span className="codicon codicon-close"></span>
|
|
</Button>
|
|
<h2 className="mb-4">
|
|
{selectedPromptTitle ||
|
|
t("prompts:systemPrompt.title", {
|
|
modeName: getCurrentMode()?.name || "Code",
|
|
})}
|
|
</h2>
|
|
<pre className="p-2 whitespace-pre-wrap break-words font-mono text-vscode-editor-font-size text-vscode-editor-foreground bg-vscode-editor-background border border-vscode-editor-lineHighlightBorder rounded overflow-y-auto">
|
|
{selectedPromptContent}
|
|
</pre>
|
|
</div>
|
|
<div className="flex justify-end p-3 px-5 border-t border-vscode-editor-lineHighlightBorder bg-vscode-editor-background">
|
|
<Button variant="secondary" onClick={() => setIsDialogOpen(false)}>
|
|
{t("prompts:createModeDialog.close")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Import Mode Dialog */}
|
|
{showImportDialog && (
|
|
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-[1000]">
|
|
<div className="bg-vscode-editor-background border border-vscode-editor-lineHighlightBorder rounded-lg shadow-lg p-6 max-w-md w-full">
|
|
<h3 className="text-lg font-semibold mb-4">{t("prompts:modes.importMode")}</h3>
|
|
<p className="text-sm text-vscode-descriptionForeground mb-4">
|
|
{t("prompts:importMode.selectLevel")}
|
|
</p>
|
|
<div className="space-y-3 mb-6">
|
|
<label className="flex items-start gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="importLevel"
|
|
value="project"
|
|
className="mt-1"
|
|
checked={importLevel === "project"}
|
|
onChange={() => setImportLevel("project")}
|
|
/>
|
|
<div>
|
|
<div className="font-medium">{t("prompts:importMode.project.label")}</div>
|
|
<div className="text-xs text-vscode-descriptionForeground">
|
|
{t("prompts:importMode.project.description")}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
<label className="flex items-start gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="importLevel"
|
|
value="global"
|
|
className="mt-1"
|
|
checked={importLevel === "global"}
|
|
onChange={() => setImportLevel("global")}
|
|
/>
|
|
<div>
|
|
<div className="font-medium">{t("prompts:importMode.global.label")}</div>
|
|
<div className="text-xs text-vscode-descriptionForeground">
|
|
{t("prompts:importMode.global.description")}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="secondary" onClick={() => setShowImportDialog(false)}>
|
|
{t("prompts:createModeDialog.buttons.cancel")}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
if (!isImporting) {
|
|
setIsImporting(true)
|
|
vscode.postMessage({
|
|
type: "importMode",
|
|
source: importLevel,
|
|
})
|
|
}
|
|
}}
|
|
disabled={isImporting}>
|
|
{isImporting ? t("prompts:importMode.importing") : t("prompts:importMode.import")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Mode Confirmation Dialog */}
|
|
<DeleteModeDialog
|
|
open={showDeleteConfirm}
|
|
onOpenChange={setShowDeleteConfirm}
|
|
modeToDelete={modeToDelete}
|
|
onConfirm={() => {
|
|
if (modeToDelete) {
|
|
vscode.postMessage({
|
|
type: "deleteCustomMode",
|
|
slug: modeToDelete.slug,
|
|
})
|
|
setShowDeleteConfirm(false)
|
|
setModeToDelete(null)
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ModesView
|