mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: implement submodule modes inheritance
This adds support for inheriting custom modes from git submodules: - Add "submodule" source type to mode schema in packages/types/src/mode.ts - Add `submodulePath` field to track which submodule a mode comes from - Add `includeSubmoduleModes` setting in global-settings.ts (default: false) - Create git-submodules.ts utility for recursive submodule detection - Modify CustomModesManager to load modes from submodules with proper precedence (project > submodule > global) - Add file watchers for submodule .roomodes files - Update UI to show submodule indicator in mode selector and make submodule modes read-only (disable rename/delete/edit buttons) Addresses Issue #10382
This commit is contained in:
parent
e851b9355e
commit
2fa52b92fd
5 changed files with 467 additions and 65 deletions
|
|
@ -179,6 +179,13 @@ export const globalSettingsSchema = z.object({
|
|||
mode: z.string().optional(),
|
||||
modeApiConfigs: z.record(z.string(), z.string()).optional(),
|
||||
customModes: z.array(modeConfigSchema).optional(),
|
||||
/**
|
||||
* Whether to include custom modes from git submodules.
|
||||
* When enabled, Roo will recursively search for .roomodes files in git submodules
|
||||
* and make those modes available in the parent workspace (as read-only).
|
||||
* @default false
|
||||
*/
|
||||
includeSubmoduleModes: z.boolean().optional(),
|
||||
customModePrompts: customModePromptsSchema.optional(),
|
||||
customSupportPrompts: customSupportPromptsSchema.optional(),
|
||||
enhancementApiConfigId: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -69,7 +69,12 @@ export const modeConfigSchema = z.object({
|
|||
description: z.string().optional(),
|
||||
customInstructions: z.string().optional(),
|
||||
groups: groupEntryArraySchema,
|
||||
source: z.enum(["global", "project"]).optional(),
|
||||
source: z.enum(["global", "project", "submodule"]).optional(),
|
||||
/**
|
||||
* Relative path to the submodule from the workspace root.
|
||||
* Only set when source is "submodule".
|
||||
*/
|
||||
submodulePath: z.string().optional(),
|
||||
})
|
||||
|
||||
export type ModeConfig = z.infer<typeof modeConfigSchema>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { logger } from "../../utils/logging"
|
|||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
|
||||
import { t } from "../../i18n"
|
||||
import { getGitSubmodules, type SubmoduleInfo } from "../../utils/git-submodules"
|
||||
|
||||
const ROOMODES_FILENAME = ".roomodes"
|
||||
|
||||
|
|
@ -48,10 +49,12 @@ export class CustomModesManager {
|
|||
private static readonly cacheTTL = 10_000
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private submoduleWatchers: vscode.Disposable[] = []
|
||||
private isWriting = false
|
||||
private writeQueue: Array<() => Promise<void>> = []
|
||||
private cachedModes: ModeConfig[] | null = null
|
||||
private cachedAt: number = 0
|
||||
private cachedSubmodules: SubmoduleInfo[] | null = null
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
|
|
@ -62,6 +65,73 @@ export class CustomModesManager {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the includeSubmoduleModes setting is enabled
|
||||
*/
|
||||
private isSubmoduleModesEnabled(): boolean {
|
||||
const includeSubmoduleModes = this.context.globalState.get<boolean>("includeSubmoduleModes")
|
||||
return includeSubmoduleModes === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads modes from a submodule's .roomodes file
|
||||
* @param submodule - The submodule info containing paths
|
||||
* @returns Array of ModeConfig with source set to "submodule" and submodulePath set
|
||||
*/
|
||||
private async loadModesFromSubmodule(submodule: SubmoduleInfo): Promise<ModeConfig[]> {
|
||||
const roomodesPath = path.join(submodule.absolutePath, ROOMODES_FILENAME)
|
||||
|
||||
if (!(await fileExistsAtPath(roomodesPath))) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const modes = await this.loadModesFromFile(roomodesPath)
|
||||
// Mark each mode as coming from a submodule
|
||||
return modes.map((mode) => ({
|
||||
...mode,
|
||||
source: "submodule" as const,
|
||||
submodulePath: submodule.fullRelativePath,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[CustomModesManager] Error loading modes from submodule ${submodule.fullRelativePath}:`,
|
||||
error,
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all modes from git submodules (recursively)
|
||||
* @returns Array of ModeConfig from all submodules
|
||||
*/
|
||||
private async getSubmoduleModes(): Promise<ModeConfig[]> {
|
||||
if (!this.isSubmoduleModesEnabled()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const submodules = await getGitSubmodules(workspaceRoot, true)
|
||||
|
||||
// Cache submodules for file watcher setup
|
||||
this.cachedSubmodules = submodules
|
||||
|
||||
const allSubmoduleModes: ModeConfig[] = []
|
||||
|
||||
for (const submodule of submodules) {
|
||||
const modes = await this.loadModesFromSubmodule(submodule)
|
||||
allSubmoduleModes.push(...modes)
|
||||
}
|
||||
|
||||
return allSubmoduleModes
|
||||
}
|
||||
|
||||
private async queueWrite(operation: () => Promise<void>): Promise<void> {
|
||||
this.writeQueue.push(operation)
|
||||
|
||||
|
|
@ -350,9 +420,76 @@ export class CustomModesManager {
|
|||
}),
|
||||
)
|
||||
this.disposables.push(roomodesWatcher)
|
||||
|
||||
// Setup submodule watchers if enabled
|
||||
await this.setupSubmoduleWatchers()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up file watchers for .roomodes files in git submodules
|
||||
* This is called during initialization and when the includeSubmoduleModes setting changes
|
||||
*/
|
||||
private async setupSubmoduleWatchers(): Promise<void> {
|
||||
// First, dispose of any existing submodule watchers
|
||||
for (const watcher of this.submoduleWatchers) {
|
||||
watcher.dispose()
|
||||
}
|
||||
this.submoduleWatchers = []
|
||||
|
||||
if (!this.isSubmoduleModesEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
|
||||
try {
|
||||
const submodules = await getGitSubmodules(workspaceRoot, true)
|
||||
|
||||
for (const submodule of submodules) {
|
||||
const roomodesPath = path.join(submodule.absolutePath, ROOMODES_FILENAME)
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(roomodesPath)
|
||||
|
||||
const handleSubmoduleChange = async () => {
|
||||
try {
|
||||
logger.info(`Submodule .roomodes changed: ${submodule.fullRelativePath}`)
|
||||
this.clearCache()
|
||||
await this.getCustomModes() // This will refresh with submodule modes
|
||||
await this.onUpdate()
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[CustomModesManager] Error handling submodule .roomodes change in ${submodule.fullRelativePath}:`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.submoduleWatchers.push(watcher.onDidChange(handleSubmoduleChange))
|
||||
this.submoduleWatchers.push(watcher.onDidCreate(handleSubmoduleChange))
|
||||
this.submoduleWatchers.push(watcher.onDidDelete(handleSubmoduleChange))
|
||||
this.submoduleWatchers.push(watcher)
|
||||
}
|
||||
|
||||
logger.info(`Set up ${submodules.length} submodule .roomodes watchers`)
|
||||
} catch (error) {
|
||||
console.error("[CustomModesManager] Failed to setup submodule watchers:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-initializes submodule watchers. Call this when the includeSubmoduleModes setting changes.
|
||||
*/
|
||||
public async refreshSubmoduleWatchers(): Promise<void> {
|
||||
await this.setupSubmoduleWatchers()
|
||||
this.clearCache()
|
||||
await this.onUpdate()
|
||||
}
|
||||
|
||||
public async getCustomModes(): Promise<ModeConfig[]> {
|
||||
// Check if we have a valid cached result.
|
||||
const now = Date.now()
|
||||
|
|
@ -369,29 +506,44 @@ export class CustomModesManager {
|
|||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
|
||||
|
||||
// Create maps to store modes by source.
|
||||
const projectModes = new Map<string, ModeConfig>()
|
||||
const globalModes = new Map<string, ModeConfig>()
|
||||
// Get modes from submodules if enabled.
|
||||
const submoduleModes = await this.getSubmoduleModes()
|
||||
|
||||
// Add project modes (they take precedence).
|
||||
// Create a set to track which slugs have been added (for deduplication).
|
||||
const addedSlugs = new Set<string>()
|
||||
|
||||
// Precedence order (highest to lowest):
|
||||
// 1. Project modes (.roomodes in workspace root)
|
||||
// 2. Submodule modes (.roomodes in submodules)
|
||||
// 3. Global modes (custom-modes.yaml)
|
||||
|
||||
const mergedModes: ModeConfig[] = []
|
||||
|
||||
// Add project modes first (highest precedence).
|
||||
for (const mode of roomodesModes) {
|
||||
projectModes.set(mode.slug, { ...mode, source: "project" as const })
|
||||
}
|
||||
|
||||
// Add global modes.
|
||||
for (const mode of settingsModes) {
|
||||
if (!projectModes.has(mode.slug)) {
|
||||
globalModes.set(mode.slug, { ...mode, source: "global" as const })
|
||||
if (!addedSlugs.has(mode.slug)) {
|
||||
addedSlugs.add(mode.slug)
|
||||
mergedModes.push({ ...mode, source: "project" as const })
|
||||
}
|
||||
}
|
||||
|
||||
// Combine modes in the correct order: project modes first, then global modes.
|
||||
const mergedModes = [
|
||||
...roomodesModes.map((mode) => ({ ...mode, source: "project" as const })),
|
||||
...settingsModes
|
||||
.filter((mode) => !projectModes.has(mode.slug))
|
||||
.map((mode) => ({ ...mode, source: "global" as const })),
|
||||
]
|
||||
// Add submodule modes (middle precedence).
|
||||
// Only add if not already defined in project modes.
|
||||
for (const mode of submoduleModes) {
|
||||
if (!addedSlugs.has(mode.slug)) {
|
||||
addedSlugs.add(mode.slug)
|
||||
mergedModes.push(mode) // Already has source: "submodule" and submodulePath set
|
||||
}
|
||||
}
|
||||
|
||||
// Add global modes (lowest precedence).
|
||||
// Only add if not already defined in project or submodule modes.
|
||||
for (const mode of settingsModes) {
|
||||
if (!addedSlugs.has(mode.slug)) {
|
||||
addedSlugs.add(mode.slug)
|
||||
mergedModes.push({ ...mode, source: "global" as const })
|
||||
}
|
||||
}
|
||||
|
||||
await this.context.globalState.update("customModes", mergedModes)
|
||||
|
||||
|
|
@ -1010,6 +1162,11 @@ export class CustomModesManager {
|
|||
disposable.dispose()
|
||||
}
|
||||
|
||||
for (const disposable of this.submoduleWatchers) {
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
this.disposables = []
|
||||
this.submoduleWatchers = []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
198
src/utils/git-submodules.ts
Normal file
198
src/utils/git-submodules.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
import { fileExistsAtPath } from "./fs"
|
||||
|
||||
/**
|
||||
* Information about a git submodule
|
||||
*/
|
||||
export interface SubmoduleInfo {
|
||||
/**
|
||||
* The name of the submodule (from the [submodule "name"] section)
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The relative path to the submodule from the parent repository root
|
||||
*/
|
||||
relativePath: string
|
||||
|
||||
/**
|
||||
* The absolute path to the submodule
|
||||
*/
|
||||
absolutePath: string
|
||||
|
||||
/**
|
||||
* For nested submodules, this is the full relative path from the workspace root
|
||||
* through all parent submodules (e.g., "parent-submodule/nested-submodule")
|
||||
*/
|
||||
fullRelativePath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a .gitmodules file and extracts submodule information
|
||||
*
|
||||
* @param gitmodulesPath - Path to the .gitmodules file
|
||||
* @param repoRoot - The root directory of the repository containing the .gitmodules file
|
||||
* @param pathPrefix - Prefix to add to relative paths (for nested submodules)
|
||||
* @returns Array of SubmoduleInfo objects
|
||||
*/
|
||||
async function parseGitmodules(
|
||||
gitmodulesPath: string,
|
||||
repoRoot: string,
|
||||
pathPrefix: string = "",
|
||||
): Promise<SubmoduleInfo[]> {
|
||||
const submodules: SubmoduleInfo[] = []
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(gitmodulesPath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
|
||||
let currentSubmodule: { name?: string; path?: string } = {}
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
// Match [submodule "name"] sections
|
||||
const submoduleMatch = trimmedLine.match(/^\[submodule\s+"([^"]+)"\]$/)
|
||||
if (submoduleMatch) {
|
||||
// Save previous submodule if it has all required fields
|
||||
if (currentSubmodule.name && currentSubmodule.path) {
|
||||
const relativePath = currentSubmodule.path
|
||||
const absolutePath = path.join(repoRoot, relativePath)
|
||||
const fullRelativePath = pathPrefix ? path.join(pathPrefix, relativePath) : relativePath
|
||||
|
||||
submodules.push({
|
||||
name: currentSubmodule.name,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
fullRelativePath,
|
||||
})
|
||||
}
|
||||
|
||||
// Start a new submodule
|
||||
currentSubmodule = { name: submoduleMatch[1] }
|
||||
continue
|
||||
}
|
||||
|
||||
// Match path = value lines
|
||||
const pathMatch = trimmedLine.match(/^path\s*=\s*(.+)$/)
|
||||
if (pathMatch && currentSubmodule.name) {
|
||||
currentSubmodule.path = pathMatch[1].trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last submodule
|
||||
if (currentSubmodule.name && currentSubmodule.path) {
|
||||
const relativePath = currentSubmodule.path
|
||||
const absolutePath = path.join(repoRoot, relativePath)
|
||||
const fullRelativePath = pathPrefix ? path.join(pathPrefix, relativePath) : relativePath
|
||||
|
||||
submodules.push({
|
||||
name: currentSubmodule.name,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
fullRelativePath,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or can't be read - return empty array
|
||||
console.error(`[git-submodules] Failed to parse ${gitmodulesPath}:`, error)
|
||||
}
|
||||
|
||||
return submodules
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all git submodules from a repository root, including nested submodules.
|
||||
*
|
||||
* This function recursively searches for .gitmodules files in the repository
|
||||
* and all its submodules to find nested submodules as well.
|
||||
*
|
||||
* @param workspaceRoot - The root directory of the workspace
|
||||
* @param recursive - Whether to recursively search for nested submodules (default: true)
|
||||
* @param maxDepth - Maximum depth for recursive search (default: 10)
|
||||
* @returns Array of SubmoduleInfo objects for all found submodules
|
||||
*/
|
||||
export async function getGitSubmodules(
|
||||
workspaceRoot: string,
|
||||
recursive: boolean = true,
|
||||
maxDepth: number = 10,
|
||||
): Promise<SubmoduleInfo[]> {
|
||||
const allSubmodules: SubmoduleInfo[] = []
|
||||
const visited = new Set<string>()
|
||||
|
||||
async function searchSubmodules(repoRoot: string, pathPrefix: string, depth: number): Promise<void> {
|
||||
if (depth > maxDepth) {
|
||||
console.warn(`[git-submodules] Max depth (${maxDepth}) reached, stopping recursive search`)
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid infinite loops with circular submodule references
|
||||
const normalizedRoot = path.normalize(repoRoot)
|
||||
if (visited.has(normalizedRoot)) {
|
||||
return
|
||||
}
|
||||
visited.add(normalizedRoot)
|
||||
|
||||
const gitmodulesPath = path.join(repoRoot, ".gitmodules")
|
||||
|
||||
// Check if .gitmodules exists
|
||||
if (!(await fileExistsAtPath(gitmodulesPath))) {
|
||||
return
|
||||
}
|
||||
|
||||
const submodules = await parseGitmodules(gitmodulesPath, repoRoot, pathPrefix)
|
||||
allSubmodules.push(...submodules)
|
||||
|
||||
// Recursively search nested submodules
|
||||
if (recursive) {
|
||||
for (const submodule of submodules) {
|
||||
// Verify the submodule directory exists before searching
|
||||
if (await fileExistsAtPath(submodule.absolutePath)) {
|
||||
await searchSubmodules(submodule.absolutePath, submodule.fullRelativePath, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await searchSubmodules(workspaceRoot, "", 0)
|
||||
|
||||
return allSubmodules
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given path is inside a git submodule
|
||||
*
|
||||
* @param filePath - The file path to check
|
||||
* @param workspaceRoot - The root directory of the workspace
|
||||
* @returns The SubmoduleInfo if the path is inside a submodule, undefined otherwise
|
||||
*/
|
||||
export async function getSubmoduleForPath(
|
||||
filePath: string,
|
||||
workspaceRoot: string,
|
||||
): Promise<SubmoduleInfo | undefined> {
|
||||
const submodules = await getGitSubmodules(workspaceRoot)
|
||||
|
||||
// Normalize the file path for comparison
|
||||
const normalizedFilePath = path.normalize(filePath)
|
||||
|
||||
// Find the most specific submodule (longest matching path)
|
||||
let matchingSubmodule: SubmoduleInfo | undefined
|
||||
|
||||
for (const submodule of submodules) {
|
||||
const normalizedSubmodulePath = path.normalize(submodule.absolutePath)
|
||||
|
||||
if (
|
||||
normalizedFilePath.startsWith(normalizedSubmodulePath + path.sep) ||
|
||||
normalizedFilePath === normalizedSubmodulePath
|
||||
) {
|
||||
// Choose the more specific (longer path) submodule
|
||||
if (!matchingSubmodule || submodule.fullRelativePath.length > matchingSubmodule.fullRelativePath.length) {
|
||||
matchingSubmodule = submodule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchingSubmodule
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ 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 ModeSource = "global" | "project" | "submodule"
|
||||
|
||||
type ImportModeResult = { type: "importModeResult"; success: boolean; slug?: string; error?: string }
|
||||
|
||||
|
|
@ -793,43 +793,53 @@ const ModesView = () => {
|
|||
.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>
|
||||
))}
|
||||
.map((modeConfig) => {
|
||||
const isSubmoduleMode = modeConfig.source === "submodule"
|
||||
return (
|
||||
<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}
|
||||
{isSubmoduleMode && (
|
||||
<span
|
||||
className="ml-2 text-xs text-vscode-descriptionForeground"
|
||||
title={`From submodule: ${modeConfig.submodulePath}`}>
|
||||
📦 {modeConfig.submodulePath}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
|
|
@ -847,26 +857,38 @@ const ModesView = () => {
|
|||
</Button>
|
||||
</StandardTooltip>
|
||||
|
||||
{/* Edit (rename) mode - only enabled for custom modes */}
|
||||
<StandardTooltip content={t("settings:providers.renameProfile")}>
|
||||
{/* Edit (rename) mode - only enabled for editable custom modes (not submodule) */}
|
||||
<StandardTooltip content={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode?.source === "submodule") {
|
||||
return t("prompts:modes.readOnlySubmodule") || "Read-only (from submodule)"
|
||||
}
|
||||
return t("settings:providers.renameProfile")
|
||||
})()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleStartRenameMode}
|
||||
data-testid="rename-mode-button"
|
||||
disabled={!findModeBySlug(visualMode, customModes)}>
|
||||
disabled={!findModeBySlug(visualMode, customModes) || findModeBySlug(visualMode, customModes)?.source === "submodule"}>
|
||||
<span className="codicon codicon-edit" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
|
||||
{/* Delete mode - disabled for built-in modes */}
|
||||
<StandardTooltip content={t("prompts:createModeDialog.deleteMode")}>
|
||||
{/* Delete mode - disabled for built-in modes and submodule modes */}
|
||||
<StandardTooltip content={(() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode?.source === "submodule") {
|
||||
return t("prompts:modes.readOnlySubmodule") || "Read-only (from submodule)"
|
||||
}
|
||||
return t("prompts:createModeDialog.deleteMode")
|
||||
})()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const customMode = findModeBySlug(visualMode, customModes)
|
||||
if (customMode) {
|
||||
if (customMode && customMode.source !== "submodule") {
|
||||
setModeToDelete({
|
||||
slug: customMode.slug,
|
||||
name: customMode.name,
|
||||
|
|
@ -880,7 +902,7 @@ const ModesView = () => {
|
|||
}
|
||||
}}
|
||||
data-testid="delete-mode-button"
|
||||
disabled={!findModeBySlug(visualMode, customModes)}>
|
||||
disabled={!findModeBySlug(visualMode, customModes) || findModeBySlug(visualMode, customModes)?.source === "submodule"}>
|
||||
<span className="codicon codicon-trash" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
|
|
@ -1106,13 +1128,26 @@ const ModesView = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Submodule mode read-only indicator */}
|
||||
{getCurrentMode()?.source === "submodule" && (
|
||||
<div className="mb-4 p-3 bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder rounded">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span>📦</span>
|
||||
<span>
|
||||
{t("prompts:modes.submoduleModeReadOnly") ||
|
||||
`This mode is from submodule "${getCurrentMode()?.submodulePath}" and is read-only.`}
|
||||
</span>
|
||||
</div>
|
||||
</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) && (
|
||||
{findModeBySlug(visualMode, customModes) && findModeBySlug(visualMode, customModes)?.source !== "submodule" && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
isToolsEditMode ? t("prompts:tools.doneEditing") : t("prompts:tools.editTools")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue