mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local>
This commit is contained in:
parent
3400499917
commit
16fbabf2a4
44 changed files with 899 additions and 37 deletions
|
|
@ -604,6 +604,7 @@ export interface WebviewMessage {
|
|||
| "requestSkills"
|
||||
| "createSkill"
|
||||
| "deleteSkill"
|
||||
| "moveSkill"
|
||||
| "openSkillFile"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
|
|
@ -639,8 +640,9 @@ export interface WebviewMessage {
|
|||
timeout?: number
|
||||
payload?: WebViewMessagePayload
|
||||
source?: "global" | "project" | "built-in"
|
||||
skillName?: string // For skill operations (createSkill, deleteSkill, openSkillFile)
|
||||
skillMode?: string // For skill operations (mode restriction)
|
||||
skillName?: string // For skill operations (createSkill, deleteSkill, moveSkill, openSkillFile)
|
||||
skillMode?: string // For skill operations (current mode restriction)
|
||||
newSkillMode?: string // For moveSkill (target mode)
|
||||
skillDescription?: string // For createSkill (skill description)
|
||||
requestId?: string
|
||||
ids?: string[]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ vi.mock("../../../i18n", () => ({
|
|||
"skills:errors.missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
|
||||
"skills:errors.manager_unavailable": "Skills manager not available",
|
||||
"skills:errors.missing_delete_fields": "Missing required fields: skillName or source",
|
||||
"skills:errors.missing_move_fields": "Missing required fields: skillName or source",
|
||||
"skills:errors.skill_not_found": `Skill "${params?.name}" not found`,
|
||||
"skills:errors.cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
|
|
@ -34,7 +36,13 @@ vi.mock("../../../i18n", () => ({
|
|||
|
||||
import * as vscode from "vscode"
|
||||
import { openFile } from "../../../integrations/misc/open-file"
|
||||
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "../skillsMessageHandler"
|
||||
import {
|
||||
handleRequestSkills,
|
||||
handleCreateSkill,
|
||||
handleDeleteSkill,
|
||||
handleMoveSkill,
|
||||
handleOpenSkillFile,
|
||||
} from "../skillsMessageHandler"
|
||||
|
||||
describe("skillsMessageHandler", () => {
|
||||
const mockLog = vi.fn()
|
||||
|
|
@ -42,6 +50,7 @@ describe("skillsMessageHandler", () => {
|
|||
const mockGetSkillsMetadata = vi.fn()
|
||||
const mockCreateSkill = vi.fn()
|
||||
const mockDeleteSkill = vi.fn()
|
||||
const mockMoveSkill = vi.fn()
|
||||
const mockGetSkill = vi.fn()
|
||||
|
||||
const createMockProvider = (hasSkillsManager: boolean = true): ClineProvider => {
|
||||
|
|
@ -50,6 +59,7 @@ describe("skillsMessageHandler", () => {
|
|||
getSkillsMetadata: mockGetSkillsMetadata,
|
||||
createSkill: mockCreateSkill,
|
||||
deleteSkill: mockDeleteSkill,
|
||||
moveSkill: mockMoveSkill,
|
||||
getSkill: mockGetSkill,
|
||||
}
|
||||
: undefined
|
||||
|
|
@ -253,6 +263,95 @@ describe("skillsMessageHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("handleMoveSkill", () => {
|
||||
it("moves a skill successfully", async () => {
|
||||
const provider = createMockProvider(true)
|
||||
mockMoveSkill.mockResolvedValue(undefined)
|
||||
mockGetSkillsMetadata.mockReturnValue([mockSkills[0]])
|
||||
|
||||
const result = await handleMoveSkill(provider, {
|
||||
type: "moveSkill",
|
||||
skillName: "test-skill",
|
||||
source: "global",
|
||||
skillMode: undefined,
|
||||
newSkillMode: "code",
|
||||
} as WebviewMessage)
|
||||
|
||||
expect(result).toEqual([mockSkills[0]])
|
||||
expect(mockMoveSkill).toHaveBeenCalledWith("test-skill", "global", undefined, "code")
|
||||
expect(mockPostMessageToWebview).toHaveBeenCalledWith({ type: "skills", skills: [mockSkills[0]] })
|
||||
})
|
||||
|
||||
it("moves a skill from one mode to another", async () => {
|
||||
const provider = createMockProvider(true)
|
||||
mockMoveSkill.mockResolvedValue(undefined)
|
||||
mockGetSkillsMetadata.mockReturnValue([mockSkills[1]])
|
||||
|
||||
const result = await handleMoveSkill(provider, {
|
||||
type: "moveSkill",
|
||||
skillName: "project-skill",
|
||||
source: "project",
|
||||
skillMode: "code",
|
||||
newSkillMode: "architect",
|
||||
} as WebviewMessage)
|
||||
|
||||
expect(result).toEqual([mockSkills[1]])
|
||||
expect(mockMoveSkill).toHaveBeenCalledWith("project-skill", "project", "code", "architect")
|
||||
})
|
||||
|
||||
it("returns undefined when required fields are missing", async () => {
|
||||
const provider = createMockProvider(true)
|
||||
|
||||
const result = await handleMoveSkill(provider, {
|
||||
type: "moveSkill",
|
||||
skillName: "test-skill",
|
||||
// missing source
|
||||
} as WebviewMessage)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Missing required fields: skillName or source")
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
||||
"Failed to move skill: Missing required fields: skillName or source",
|
||||
)
|
||||
})
|
||||
|
||||
it("returns undefined when skills manager is not available", async () => {
|
||||
const provider = createMockProvider(false)
|
||||
|
||||
const result = await handleMoveSkill(provider, {
|
||||
type: "moveSkill",
|
||||
skillName: "test-skill",
|
||||
source: "global",
|
||||
newSkillMode: "code",
|
||||
} as WebviewMessage)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(mockLog).toHaveBeenCalledWith("Error moving skill: Skills manager not available")
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
||||
"Failed to move skill: Skills manager not available",
|
||||
)
|
||||
})
|
||||
|
||||
it("returns undefined when trying to move a built-in skill", async () => {
|
||||
const provider = createMockProvider(true)
|
||||
|
||||
const result = await handleMoveSkill(provider, {
|
||||
type: "moveSkill",
|
||||
skillName: "test-skill",
|
||||
source: "built-in",
|
||||
newSkillMode: "code",
|
||||
} as WebviewMessage)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
"Error moving skill: Built-in skills cannot be created, deleted, or moved",
|
||||
)
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
|
||||
"Failed to move skill: Built-in skills cannot be created, deleted, or moved",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleOpenSkillFile", () => {
|
||||
it("opens a skill file successfully", async () => {
|
||||
const provider = createMockProvider(true)
|
||||
|
|
|
|||
|
|
@ -111,6 +111,47 @@ export async function handleDeleteSkill(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the moveSkill message - moves a skill to a different mode
|
||||
*/
|
||||
export async function handleMoveSkill(
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
): Promise<SkillMetadata[] | undefined> {
|
||||
try {
|
||||
const skillName = message.skillName
|
||||
const source = message.source
|
||||
const currentMode = message.skillMode
|
||||
const newMode = message.newSkillMode
|
||||
|
||||
if (!skillName || !source) {
|
||||
throw new Error(t("skills:errors.missing_move_fields"))
|
||||
}
|
||||
|
||||
// Built-in skills cannot be moved
|
||||
if (source === "built-in") {
|
||||
throw new Error(t("skills:errors.cannot_modify_builtin"))
|
||||
}
|
||||
|
||||
const skillsManager = provider.getSkillsManager()
|
||||
if (!skillsManager) {
|
||||
throw new Error(t("skills:errors.manager_unavailable"))
|
||||
}
|
||||
|
||||
await skillsManager.moveSkill(skillName, source, currentMode, newMode)
|
||||
|
||||
// Send updated skills list
|
||||
const skills = skillsManager.getSkillsMetadata()
|
||||
await provider.postMessageToWebview({ type: "skills", skills })
|
||||
return skills
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
provider.log(`Error moving skill: ${errorMessage}`)
|
||||
vscode.window.showErrorMessage(`Failed to move skill: ${errorMessage}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the openSkillFile message - opens a skill file in the editor
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -32,7 +32,13 @@ import { ClineProvider } from "./ClineProvider"
|
|||
import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
|
||||
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
|
||||
import { generateErrorDiagnostics } from "./diagnosticsHandler"
|
||||
import { handleRequestSkills, handleCreateSkill, handleDeleteSkill, handleOpenSkillFile } from "./skillsMessageHandler"
|
||||
import {
|
||||
handleRequestSkills,
|
||||
handleCreateSkill,
|
||||
handleDeleteSkill,
|
||||
handleMoveSkill,
|
||||
handleOpenSkillFile,
|
||||
} from "./skillsMessageHandler"
|
||||
import { changeLanguage, t } from "../../i18n"
|
||||
import { Package } from "../../shared/package"
|
||||
import { type RouterName, toRouterName } from "../../shared/api"
|
||||
|
|
@ -2982,6 +2988,10 @@ export const webviewMessageHandler = async (
|
|||
await handleDeleteSkill(provider, message)
|
||||
break
|
||||
}
|
||||
case "moveSkill": {
|
||||
await handleMoveSkill(provider, message)
|
||||
break
|
||||
}
|
||||
case "openSkillFile": {
|
||||
await handleOpenSkillFile(provider, message)
|
||||
break
|
||||
|
|
|
|||
1
src/i18n/locales/ca/skills.json
generated
1
src/i18n/locales/ca/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "L'habilitat \"{{name}}\" ja existeix a {{path}}",
|
||||
"not_found": "No s'ha trobat l'habilitat \"{{name}}\" a {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Falten camps obligatoris: skillName, source o skillDescription",
|
||||
"missing_move_fields": "Falten camps obligatoris: skillName o source",
|
||||
"manager_unavailable": "El gestor d'habilitats no està disponible",
|
||||
"missing_delete_fields": "Falten camps obligatoris: skillName o source",
|
||||
"skill_not_found": "No s'ha trobat l'habilitat \"{{name}}\"",
|
||||
|
|
|
|||
1
src/i18n/locales/de/skills.json
generated
1
src/i18n/locales/de/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Skill \"{{name}}\" existiert bereits unter {{path}}",
|
||||
"not_found": "Skill \"{{name}}\" nicht gefunden in {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Erforderliche Felder fehlen: skillName, source oder skillDescription",
|
||||
"missing_move_fields": "Erforderliche Felder fehlen: skillName oder source",
|
||||
"manager_unavailable": "Skill-Manager nicht verfügbar",
|
||||
"missing_delete_fields": "Erforderliche Felder fehlen: skillName oder source",
|
||||
"skill_not_found": "Skill \"{{name}}\" nicht gefunden",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
"already_exists": "Skill \"{{name}}\" already exists at {{path}}",
|
||||
"not_found": "Skill \"{{name}}\" not found in {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Missing required fields: skillName, source, or skillDescription",
|
||||
"missing_move_fields": "Missing required fields: skillName or source",
|
||||
"manager_unavailable": "Skills manager not available",
|
||||
"missing_delete_fields": "Missing required fields: skillName or source",
|
||||
"skill_not_found": "Skill \"{{name}}\" not found",
|
||||
"cannot_modify_builtin": "Built-in skills cannot be created or deleted",
|
||||
"cannot_modify_builtin": "Built-in skills cannot be created, deleted, or moved",
|
||||
"cannot_open_builtin": "Built-in skills cannot be opened as files"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
src/i18n/locales/es/skills.json
generated
1
src/i18n/locales/es/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "La habilidad \"{{name}}\" ya existe en {{path}}",
|
||||
"not_found": "No se encontró la habilidad \"{{name}}\" en {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Faltan campos obligatorios: skillName, source o skillDescription",
|
||||
"missing_move_fields": "Faltan campos obligatorios: skillName o source",
|
||||
"manager_unavailable": "El gestor de habilidades no está disponible",
|
||||
"missing_delete_fields": "Faltan campos obligatorios: skillName o source",
|
||||
"skill_not_found": "No se encontró la habilidad \"{{name}}\"",
|
||||
|
|
|
|||
1
src/i18n/locales/fr/skills.json
generated
1
src/i18n/locales/fr/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "La compétence \"{{name}}\" existe déjà à {{path}}",
|
||||
"not_found": "Compétence \"{{name}}\" introuvable dans {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Champs obligatoires manquants : skillName, source ou skillDescription",
|
||||
"missing_move_fields": "Champs obligatoires manquants : skillName ou source",
|
||||
"manager_unavailable": "Le gestionnaire de compétences n'est pas disponible",
|
||||
"missing_delete_fields": "Champs obligatoires manquants : skillName ou source",
|
||||
"skill_not_found": "Compétence \"{{name}}\" introuvable",
|
||||
|
|
|
|||
1
src/i18n/locales/hi/skills.json
generated
1
src/i18n/locales/hi/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "स्किल \"{{name}}\" पहले से {{path}} पर मौजूद है",
|
||||
"not_found": "स्किल \"{{name}}\" {{source}}{{modeInfo}} में नहीं मिला",
|
||||
"missing_create_fields": "आवश्यक फ़ील्ड गायब हैं: skillName, source, या skillDescription",
|
||||
"missing_move_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source",
|
||||
"manager_unavailable": "स्किल मैनेजर उपलब्ध नहीं है",
|
||||
"missing_delete_fields": "आवश्यक फ़ील्ड गायब हैं: skillName या source",
|
||||
"skill_not_found": "स्किल \"{{name}}\" नहीं मिला",
|
||||
|
|
|
|||
1
src/i18n/locales/id/skills.json
generated
1
src/i18n/locales/id/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Skill \"{{name}}\" sudah ada di {{path}}",
|
||||
"not_found": "Skill \"{{name}}\" tidak ditemukan di {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Bidang wajib tidak ada: skillName, source, atau skillDescription",
|
||||
"missing_move_fields": "Bidang wajib tidak ada: skillName atau source",
|
||||
"manager_unavailable": "Manajer skill tidak tersedia",
|
||||
"missing_delete_fields": "Bidang wajib tidak ada: skillName atau source",
|
||||
"skill_not_found": "Skill \"{{name}}\" tidak ditemukan",
|
||||
|
|
|
|||
1
src/i18n/locales/it/skills.json
generated
1
src/i18n/locales/it/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "La skill \"{{name}}\" esiste già in {{path}}",
|
||||
"not_found": "Skill \"{{name}}\" non trovata in {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Campi obbligatori mancanti: skillName, source o skillDescription",
|
||||
"missing_move_fields": "Campi obbligatori mancanti: skillName o source",
|
||||
"manager_unavailable": "Il gestore delle skill non è disponibile",
|
||||
"missing_delete_fields": "Campi obbligatori mancanti: skillName o source",
|
||||
"skill_not_found": "Skill \"{{name}}\" non trovata",
|
||||
|
|
|
|||
1
src/i18n/locales/ja/skills.json
generated
1
src/i18n/locales/ja/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "スキル「{{name}}」は既に{{path}}に存在します",
|
||||
"not_found": "スキル「{{name}}」が{{source}}{{modeInfo}}に見つかりません",
|
||||
"missing_create_fields": "必須フィールドが不足しています:skillName、source、またはskillDescription",
|
||||
"missing_move_fields": "必須フィールドが不足しています:skillNameまたはsource",
|
||||
"manager_unavailable": "スキルマネージャーが利用できません",
|
||||
"missing_delete_fields": "必須フィールドが不足しています:skillNameまたはsource",
|
||||
"skill_not_found": "スキル「{{name}}」が見つかりません",
|
||||
|
|
|
|||
1
src/i18n/locales/ko/skills.json
generated
1
src/i18n/locales/ko/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "스킬 \"{{name}}\"이(가) 이미 {{path}}에 존재합니다",
|
||||
"not_found": "{{source}}{{modeInfo}}에서 스킬 \"{{name}}\"을(를) 찾을 수 없습니다",
|
||||
"missing_create_fields": "필수 필드 누락: skillName, source 또는 skillDescription",
|
||||
"missing_move_fields": "필수 필드 누락: skillName 또는 source",
|
||||
"manager_unavailable": "스킬 관리자를 사용할 수 없습니다",
|
||||
"missing_delete_fields": "필수 필드 누락: skillName 또는 source",
|
||||
"skill_not_found": "스킬 \"{{name}}\"을(를) 찾을 수 없습니다",
|
||||
|
|
|
|||
1
src/i18n/locales/nl/skills.json
generated
1
src/i18n/locales/nl/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Vaardigheid \"{{name}}\" bestaat al op {{path}}",
|
||||
"not_found": "Vaardigheid \"{{name}}\" niet gevonden in {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Vereiste velden ontbreken: skillName, source of skillDescription",
|
||||
"missing_move_fields": "Vereiste velden ontbreken: skillName of source",
|
||||
"manager_unavailable": "Vaardigheidenbeheerder niet beschikbaar",
|
||||
"missing_delete_fields": "Vereiste velden ontbreken: skillName of source",
|
||||
"skill_not_found": "Vaardigheid \"{{name}}\" niet gevonden",
|
||||
|
|
|
|||
1
src/i18n/locales/pl/skills.json
generated
1
src/i18n/locales/pl/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Umiejętność \"{{name}}\" już istnieje w {{path}}",
|
||||
"not_found": "Nie znaleziono umiejętności \"{{name}}\" w {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Brakuje wymaganych pól: skillName, source lub skillDescription",
|
||||
"missing_move_fields": "Brakuje wymaganych pól: skillName lub source",
|
||||
"manager_unavailable": "Menedżer umiejętności niedostępny",
|
||||
"missing_delete_fields": "Brakuje wymaganych pól: skillName lub source",
|
||||
"skill_not_found": "Nie znaleziono umiejętności \"{{name}}\"",
|
||||
|
|
|
|||
1
src/i18n/locales/pt-BR/skills.json
generated
1
src/i18n/locales/pt-BR/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "A habilidade \"{{name}}\" já existe em {{path}}",
|
||||
"not_found": "Habilidade \"{{name}}\" não encontrada em {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Campos obrigatórios ausentes: skillName, source ou skillDescription",
|
||||
"missing_move_fields": "Campos obrigatórios ausentes: skillName ou source",
|
||||
"manager_unavailable": "Gerenciador de habilidades não disponível",
|
||||
"missing_delete_fields": "Campos obrigatórios ausentes: skillName ou source",
|
||||
"skill_not_found": "Habilidade \"{{name}}\" não encontrada",
|
||||
|
|
|
|||
1
src/i18n/locales/ru/skills.json
generated
1
src/i18n/locales/ru/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Навык \"{{name}}\" уже существует в {{path}}",
|
||||
"not_found": "Навык \"{{name}}\" не найден в {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Отсутствуют обязательные поля: skillName, source или skillDescription",
|
||||
"missing_move_fields": "Отсутствуют обязательные поля: skillName или source",
|
||||
"manager_unavailable": "Менеджер навыков недоступен",
|
||||
"missing_delete_fields": "Отсутствуют обязательные поля: skillName или source",
|
||||
"skill_not_found": "Навык \"{{name}}\" не найден",
|
||||
|
|
|
|||
1
src/i18n/locales/tr/skills.json
generated
1
src/i18n/locales/tr/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "\"{{name}}\" becerisi zaten {{path}} konumunda mevcut",
|
||||
"not_found": "\"{{name}}\" becerisi {{source}}{{modeInfo}} içinde bulunamadı",
|
||||
"missing_create_fields": "Gerekli alanlar eksik: skillName, source veya skillDescription",
|
||||
"missing_move_fields": "Gerekli alanlar eksik: skillName veya source",
|
||||
"manager_unavailable": "Beceri yöneticisi kullanılamıyor",
|
||||
"missing_delete_fields": "Gerekli alanlar eksik: skillName veya source",
|
||||
"skill_not_found": "\"{{name}}\" becerisi bulunamadı",
|
||||
|
|
|
|||
1
src/i18n/locales/vi/skills.json
generated
1
src/i18n/locales/vi/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "Kỹ năng \"{{name}}\" đã tồn tại tại {{path}}",
|
||||
"not_found": "Không tìm thấy kỹ năng \"{{name}}\" trong {{source}}{{modeInfo}}",
|
||||
"missing_create_fields": "Thiếu các trường bắt buộc: skillName, source hoặc skillDescription",
|
||||
"missing_move_fields": "Thiếu các trường bắt buộc: skillName hoặc source",
|
||||
"manager_unavailable": "Trình quản lý kỹ năng không khả dụng",
|
||||
"missing_delete_fields": "Thiếu các trường bắt buộc: skillName hoặc source",
|
||||
"skill_not_found": "Không tìm thấy kỹ năng \"{{name}}\"",
|
||||
|
|
|
|||
1
src/i18n/locales/zh-CN/skills.json
generated
1
src/i18n/locales/zh-CN/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "技能 \"{{name}}\" 已存在于 {{path}}",
|
||||
"not_found": "在 {{source}}{{modeInfo}} 中未找到技能 \"{{name}}\"",
|
||||
"missing_create_fields": "缺少必填字段:skillName、source 或 skillDescription",
|
||||
"missing_move_fields": "缺少必填字段:skillName 或 source",
|
||||
"manager_unavailable": "技能管理器不可用",
|
||||
"missing_delete_fields": "缺少必填字段:skillName 或 source",
|
||||
"skill_not_found": "未找到技能 \"{{name}}\"",
|
||||
|
|
|
|||
1
src/i18n/locales/zh-TW/skills.json
generated
1
src/i18n/locales/zh-TW/skills.json
generated
|
|
@ -7,6 +7,7 @@
|
|||
"already_exists": "技能「{{name}}」已存在於 {{path}}",
|
||||
"not_found": "在 {{source}}{{modeInfo}} 中找不到技能「{{name}}」",
|
||||
"missing_create_fields": "缺少必填欄位:skillName、source 或 skillDescription",
|
||||
"missing_move_fields": "缺少必填欄位:skillName 或 source",
|
||||
"manager_unavailable": "技能管理器無法使用",
|
||||
"missing_delete_fields": "缺少必填欄位:skillName 或 source",
|
||||
"skill_not_found": "找不到技能「{{name}}」",
|
||||
|
|
|
|||
|
|
@ -400,6 +400,77 @@ Add your skill instructions here.
|
|||
await this.discoverSkills()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a skill to a different mode
|
||||
* @param name - Skill name to move
|
||||
* @param source - Where the skill is located ("global" or "project")
|
||||
* @param currentMode - Current mode (undefined for generic skills)
|
||||
* @param newMode - Target mode (undefined for generic skills)
|
||||
*/
|
||||
async moveSkill(
|
||||
name: string,
|
||||
source: "global" | "project",
|
||||
currentMode: string | undefined,
|
||||
newMode: string | undefined,
|
||||
): Promise<void> {
|
||||
// Don't move if source and destination are the same
|
||||
if (currentMode === newMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the skill at its current location
|
||||
const skill = this.getSkill(name, source, currentMode)
|
||||
if (!skill) {
|
||||
const modeInfo = currentMode ? ` (mode: ${currentMode})` : ""
|
||||
throw new Error(t("skills:errors.not_found", { name, source, modeInfo }))
|
||||
}
|
||||
|
||||
// Determine base directory
|
||||
let baseDir: string
|
||||
if (source === "global") {
|
||||
baseDir = getGlobalRooDirectory()
|
||||
} else {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider?.cwd) {
|
||||
throw new Error(t("skills:errors.no_workspace"))
|
||||
}
|
||||
baseDir = path.join(provider.cwd, ".roo")
|
||||
}
|
||||
|
||||
// Determine source and destination directories
|
||||
const sourceDirName = currentMode ? `skills-${currentMode}` : "skills"
|
||||
const destDirName = newMode ? `skills-${newMode}` : "skills"
|
||||
const sourceDir = path.join(baseDir, sourceDirName, name)
|
||||
const destSkillsDir = path.join(baseDir, destDirName)
|
||||
const destDir = path.join(destSkillsDir, name)
|
||||
const destSkillMdPath = path.join(destDir, "SKILL.md")
|
||||
|
||||
// Check if skill already exists at destination
|
||||
if (await fileExists(destSkillMdPath)) {
|
||||
throw new Error(t("skills:errors.already_exists", { name, path: destSkillMdPath }))
|
||||
}
|
||||
|
||||
// Ensure destination skills directory exists
|
||||
await fs.mkdir(destSkillsDir, { recursive: true })
|
||||
|
||||
// Move the skill directory
|
||||
await fs.rename(sourceDir, destDir)
|
||||
|
||||
// Clean up empty source skills directory
|
||||
const sourceSkillsDir = path.join(baseDir, sourceDirName)
|
||||
try {
|
||||
const entries = await fs.readdir(sourceSkillsDir)
|
||||
if (entries.length === 0) {
|
||||
await fs.rmdir(sourceSkillsDir)
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors - directory might not exist or have permission issues
|
||||
}
|
||||
|
||||
// Refresh skills list
|
||||
await this.discoverSkills()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all skills directories to scan, including mode-specific directories.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const {
|
|||
mockMkdir,
|
||||
mockWriteFile,
|
||||
mockRm,
|
||||
mockRename,
|
||||
mockRmdir,
|
||||
} = vi.hoisted(() => ({
|
||||
mockStat: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
|
|
@ -23,6 +25,8 @@ const {
|
|||
mockMkdir: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockRm: vi.fn(),
|
||||
mockRename: vi.fn(),
|
||||
mockRmdir: vi.fn(),
|
||||
}))
|
||||
|
||||
// Platform-agnostic test paths
|
||||
|
|
@ -44,6 +48,8 @@ vi.mock("fs/promises", () => ({
|
|||
mkdir: mockMkdir,
|
||||
writeFile: mockWriteFile,
|
||||
rm: mockRm,
|
||||
rename: mockRename,
|
||||
rmdir: mockRmdir,
|
||||
},
|
||||
stat: mockStat,
|
||||
readFile: mockReadFile,
|
||||
|
|
@ -52,6 +58,8 @@ vi.mock("fs/promises", () => ({
|
|||
mkdir: mockMkdir,
|
||||
writeFile: mockWriteFile,
|
||||
rm: mockRm,
|
||||
rename: mockRename,
|
||||
rmdir: mockRmdir,
|
||||
}))
|
||||
|
||||
// Mock os module
|
||||
|
|
@ -1134,4 +1142,402 @@ Instructions`)
|
|||
await expect(skillsManager.deleteSkill("non-existent", "global")).rejects.toThrow("not found")
|
||||
})
|
||||
})
|
||||
|
||||
describe("moveSkill", () => {
|
||||
it("should move a skill from generic to mode-specific directory", async () => {
|
||||
const sourceDir = p(globalSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(GLOBAL_ROO_DIR, "skills-code", "test-skill")
|
||||
const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-code")
|
||||
|
||||
// Setup: skill exists in generic skills directory
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalSkillsDir) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in source
|
||||
if (file === testSkillMd) return true
|
||||
// Skill does not exist in destination
|
||||
if (file === p(destDir, "SKILL.md")) return false
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockRename.mockResolvedValue(undefined)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Verify skill exists
|
||||
expect(skillsManager.getSkill("test-skill", "global")).toBeDefined()
|
||||
|
||||
// Move the skill to code mode
|
||||
await skillsManager.moveSkill("test-skill", "global", undefined, "code")
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(destSkillsDir, { recursive: true })
|
||||
expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir)
|
||||
})
|
||||
|
||||
it("should move a skill from one mode to another", async () => {
|
||||
const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code")
|
||||
const sourceDir = p(sourceSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill")
|
||||
const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect")
|
||||
|
||||
// Setup: skill exists in code mode directory
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === sourceSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === sourceSkillsDir) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in source
|
||||
if (file === testSkillMd) return true
|
||||
// Skill does not exist in destination
|
||||
if (file === p(destDir, "SKILL.md")) return false
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockRename.mockResolvedValue(undefined)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Verify skill exists with mode
|
||||
expect(skillsManager.getSkill("test-skill", "global", "code")).toBeDefined()
|
||||
|
||||
// Move the skill to architect mode
|
||||
await skillsManager.moveSkill("test-skill", "global", "code", "architect")
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(destSkillsDir, { recursive: true })
|
||||
expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir)
|
||||
})
|
||||
|
||||
it("should move a skill from mode-specific to generic directory", async () => {
|
||||
const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code")
|
||||
const sourceDir = p(sourceSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(globalSkillsDir, "test-skill")
|
||||
|
||||
// Setup: skill exists in code mode directory
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === sourceSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === sourceSkillsDir) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in source
|
||||
if (file === testSkillMd) return true
|
||||
// Skill does not exist in destination
|
||||
if (file === p(destDir, "SKILL.md")) return false
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockRename.mockResolvedValue(undefined)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Verify skill exists with mode
|
||||
expect(skillsManager.getSkill("test-skill", "global", "code")).toBeDefined()
|
||||
|
||||
// Move the skill to generic (no mode)
|
||||
await skillsManager.moveSkill("test-skill", "global", "code", undefined)
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(globalSkillsDir, { recursive: true })
|
||||
expect(mockRename).toHaveBeenCalledWith(sourceDir, destDir)
|
||||
})
|
||||
|
||||
it("should not do anything when source and destination modes are the same", async () => {
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalSkillsDir) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const testSkillDir = p(globalSkillsDir, "test-skill")
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === testSkillDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
return file === p(testSkillDir, "SKILL.md")
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Try to move skill to the same mode (undefined -> undefined)
|
||||
await skillsManager.moveSkill("test-skill", "global", undefined, undefined)
|
||||
|
||||
// Should not call rename
|
||||
expect(mockRename).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should throw error if skill does not exist", async () => {
|
||||
mockDirectoryExists.mockResolvedValue(false)
|
||||
mockRealpath.mockImplementation(async (p: string) => p)
|
||||
mockReaddir.mockResolvedValue([])
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
await expect(skillsManager.moveSkill("non-existent", "global", undefined, "code")).rejects.toThrow(
|
||||
"not found",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error if skill already exists at destination", async () => {
|
||||
const sourceDir = p(globalSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(GLOBAL_ROO_DIR, "skills-code", "test-skill")
|
||||
const destSkillMd = p(destDir, "SKILL.md")
|
||||
|
||||
// Setup: skill exists in both locations
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === globalSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === globalSkillsDir) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in both source and destination
|
||||
if (file === testSkillMd) return true
|
||||
if (file === destSkillMd) return true
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
await expect(skillsManager.moveSkill("test-skill", "global", undefined, "code")).rejects.toThrow(
|
||||
"already exists",
|
||||
)
|
||||
})
|
||||
|
||||
it("should clean up empty source skills directory after moving", async () => {
|
||||
const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code")
|
||||
const sourceDir = p(sourceSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill")
|
||||
const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect")
|
||||
|
||||
// Setup: skill exists in code mode directory
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === sourceSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
// Track readdir calls - return skill for discovery, empty for cleanup check
|
||||
let readdirCallCount = 0
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === sourceSkillsDir) {
|
||||
readdirCallCount++
|
||||
// First call is for discovery, return the skill
|
||||
// Second call is for cleanup check after move, return empty
|
||||
if (readdirCallCount === 1) {
|
||||
return ["test-skill"]
|
||||
}
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in source
|
||||
if (file === testSkillMd) return true
|
||||
// Skill does not exist in destination
|
||||
if (file === p(destDir, "SKILL.md")) return false
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockRename.mockResolvedValue(undefined)
|
||||
mockRmdir.mockResolvedValue(undefined)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Move the skill to architect mode
|
||||
await skillsManager.moveSkill("test-skill", "global", "code", "architect")
|
||||
|
||||
// Verify empty directory was cleaned up
|
||||
expect(mockRmdir).toHaveBeenCalledWith(sourceSkillsDir)
|
||||
})
|
||||
|
||||
it("should not clean up source skills directory if it still has other skills", async () => {
|
||||
const sourceSkillsDir = p(GLOBAL_ROO_DIR, "skills-code")
|
||||
const sourceDir = p(sourceSkillsDir, "test-skill")
|
||||
const testSkillMd = p(sourceDir, "SKILL.md")
|
||||
const destDir = p(GLOBAL_ROO_DIR, "skills-architect", "test-skill")
|
||||
const destSkillsDir = p(GLOBAL_ROO_DIR, "skills-architect")
|
||||
|
||||
// Setup: skill exists in code mode directory along with another skill
|
||||
mockDirectoryExists.mockImplementation(async (dir: string) => {
|
||||
return dir === sourceSkillsDir
|
||||
})
|
||||
|
||||
mockRealpath.mockImplementation(async (pathArg: string) => pathArg)
|
||||
|
||||
// Track readdir calls - return skill for discovery, non-empty for cleanup check
|
||||
let readdirCallCount = 0
|
||||
mockReaddir.mockImplementation(async (dir: string) => {
|
||||
if (dir === sourceSkillsDir) {
|
||||
readdirCallCount++
|
||||
// First call is for discovery
|
||||
if (readdirCallCount === 1) {
|
||||
return ["test-skill", "another-skill"]
|
||||
}
|
||||
// Second call for cleanup - still has another skill
|
||||
return ["another-skill"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
mockStat.mockImplementation(async (pathArg: string) => {
|
||||
if (pathArg === sourceDir || pathArg === p(sourceSkillsDir, "another-skill")) {
|
||||
return { isDirectory: () => true }
|
||||
}
|
||||
throw new Error("Not found")
|
||||
})
|
||||
|
||||
mockFileExists.mockImplementation(async (file: string) => {
|
||||
// Skill exists in source
|
||||
if (file === testSkillMd) return true
|
||||
if (file === p(sourceSkillsDir, "another-skill", "SKILL.md")) return true
|
||||
// Skill does not exist in destination
|
||||
if (file === p(destDir, "SKILL.md")) return false
|
||||
return false
|
||||
})
|
||||
|
||||
mockReadFile.mockResolvedValue(`---
|
||||
name: test-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions`)
|
||||
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockRename.mockResolvedValue(undefined)
|
||||
mockRmdir.mockResolvedValue(undefined)
|
||||
|
||||
await skillsManager.discoverSkills()
|
||||
|
||||
// Move the skill to architect mode
|
||||
await skillsManager.moveSkill("test-skill", "global", "code", "architect")
|
||||
|
||||
// Verify directory was NOT cleaned up (still has other skills)
|
||||
expect(mockRmdir).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import React from "react"
|
||||
import React, { useCallback, useMemo } from "react"
|
||||
import { Edit, Trash2 } from "lucide-react"
|
||||
|
||||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { getAllModes } from "@roo/modes"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Button, StandardTooltip } from "@/components/ui"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { Button, StandardTooltip, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
// Sentinel value for "Any mode" since Radix Select doesn't allow empty string values
|
||||
const MODE_ANY = "__any__"
|
||||
|
||||
interface SkillItemProps {
|
||||
skill: SkillMetadata
|
||||
|
|
@ -14,6 +21,40 @@ interface SkillItemProps {
|
|||
|
||||
export const SkillItem: React.FC<SkillItemProps> = ({ skill, onEdit, onDelete }) => {
|
||||
const { t } = useAppTranslation()
|
||||
const { customModes } = useExtensionState()
|
||||
|
||||
// Get available modes for the dropdown (built-in + custom modes)
|
||||
const availableModes = useMemo(() => {
|
||||
return getAllModes(customModes).map((m) => ({ slug: m.slug, name: m.name }))
|
||||
}, [customModes])
|
||||
|
||||
// Current mode value for the select (using sentinel for "Any mode")
|
||||
const currentModeValue = skill.mode || MODE_ANY
|
||||
|
||||
// Handle mode change
|
||||
const handleModeChange = useCallback(
|
||||
(newModeValue: string) => {
|
||||
const newMode = newModeValue === MODE_ANY ? undefined : newModeValue
|
||||
|
||||
// Don't do anything if mode hasn't changed
|
||||
if (newMode === skill.mode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send message to move skill to new mode
|
||||
vscode.postMessage({
|
||||
type: "moveSkill",
|
||||
skillName: skill.name,
|
||||
source: skill.source,
|
||||
skillMode: skill.mode,
|
||||
newSkillMode: newMode,
|
||||
})
|
||||
},
|
||||
[skill.name, skill.source, skill.mode],
|
||||
)
|
||||
|
||||
// Built-in skills cannot change mode
|
||||
const isBuiltIn = skill.source === "built-in"
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2 text-sm flex items-center group hover:bg-vscode-list-hoverBackground">
|
||||
|
|
@ -21,17 +62,37 @@ export const SkillItem: React.FC<SkillItemProps> = ({ skill, onEdit, onDelete })
|
|||
<div className="flex-1 min-w-0 cursor-pointer" onClick={onEdit}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-vscode-foreground">{skill.name}</span>
|
||||
{skill.mode && (
|
||||
<span className="px-1.5 py-0.5 text-xs rounded bg-vscode-badge-background text-vscode-badge-foreground shrink-0">
|
||||
{skill.mode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{skill.description && (
|
||||
<div className="text-xs text-vscode-descriptionForeground truncate mt-0.5">{skill.description}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mode dropdown */}
|
||||
<div className="ml-2 shrink-0">
|
||||
{isBuiltIn ? (
|
||||
<span className="px-1.5 py-0.5 text-xs rounded bg-vscode-badge-background text-vscode-badge-foreground">
|
||||
{skill.mode || t("settings:skills.modeAny")}
|
||||
</span>
|
||||
) : (
|
||||
<StandardTooltip content={t("settings:skills.changeMode")}>
|
||||
<Select value={currentModeValue} onValueChange={handleModeChange}>
|
||||
<SelectTrigger className="h-6 w-auto min-w-[80px] max-w-[120px] text-xs px-2 py-0.5 border-none bg-vscode-badge-background text-vscode-badge-foreground hover:bg-vscode-button-hoverBackground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={MODE_ANY}>{t("settings:skills.modeAny")}</SelectItem>
|
||||
{availableModes.map((m) => (
|
||||
<SelectItem key={m.slug} value={m.slug}>
|
||||
{m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<StandardTooltip content={t("settings:skills.editSkill")}>
|
||||
|
|
@ -45,16 +106,18 @@ export const SkillItem: React.FC<SkillItemProps> = ({ skill, onEdit, onDelete })
|
|||
</Button>
|
||||
</StandardTooltip>
|
||||
|
||||
<StandardTooltip content={t("settings:skills.deleteSkill")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
tabIndex={-1}
|
||||
onClick={onDelete}
|
||||
className="size-6 flex items-center justify-center opacity-60 hover:opacity-100 hover:text-red-400">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
{!isBuiltIn && (
|
||||
<StandardTooltip content={t("settings:skills.deleteSkill")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
tabIndex={-1}
|
||||
onClick={onDelete}
|
||||
className="size-6 flex items-center justify-center opacity-60 hover:opacity-100 hover:text-red-400">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { render, screen, fireEvent } from "@/utils/test-utils"
|
|||
import type { SkillMetadata } from "@roo-code/types"
|
||||
|
||||
import { SkillItem } from "../SkillItem"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
|
|
@ -14,11 +15,35 @@ vi.mock("@/utils/vscode", () => ({
|
|||
// Mock the translation hook
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"settings:skills.editSkill": "Edit skill",
|
||||
"settings:skills.deleteSkill": "Delete skill",
|
||||
"settings:skills.changeMode": "Change mode",
|
||||
"settings:skills.modeAny": "Any mode",
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock UI components
|
||||
// Mock getAllModes
|
||||
vi.mock("@roo/modes", () => ({
|
||||
getAllModes: () => [
|
||||
{ slug: "code", name: "💻 Code" },
|
||||
{ slug: "architect", name: "🏗️ Architect" },
|
||||
{ slug: "ask", name: "❓ Ask" },
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock useExtensionState
|
||||
vi.mock("@/context/ExtensionStateContext", () => ({
|
||||
useExtensionState: () => ({
|
||||
customModes: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock UI components - need to support Select components
|
||||
vi.mock("@/components/ui", () => ({
|
||||
Button: ({ children, onClick, className, tabIndex }: any) => (
|
||||
<button onClick={onClick} className={className} tabIndex={tabIndex} data-testid="button">
|
||||
|
|
@ -30,6 +55,26 @@ vi.mock("@/components/ui", () => ({
|
|||
{children}
|
||||
</div>
|
||||
),
|
||||
Select: ({ children, value, onValueChange }: any) => (
|
||||
<div data-testid="select" data-value={value}>
|
||||
{children}
|
||||
<button data-testid="select-change-button" onClick={() => onValueChange("code")}>
|
||||
Change to code
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children }: any) => <div data-testid="select-content">{children}</div>,
|
||||
SelectItem: ({ children, value }: any) => (
|
||||
<div data-testid="select-item" data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children, className }: any) => (
|
||||
<button data-testid="select-trigger" className={className}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SelectValue: () => <span data-testid="select-value">Value</span>,
|
||||
}))
|
||||
|
||||
const mockSkill: SkillMetadata = {
|
||||
|
|
@ -47,6 +92,13 @@ const mockSkillWithMode: SkillMetadata = {
|
|||
mode: "architect",
|
||||
}
|
||||
|
||||
const mockBuiltInSkill: SkillMetadata = {
|
||||
name: "built-in-skill",
|
||||
description: "A built-in skill",
|
||||
path: "<built-in:test>",
|
||||
source: "built-in",
|
||||
}
|
||||
|
||||
describe("SkillItem", () => {
|
||||
const mockOnEdit = vi.fn()
|
||||
const mockOnDelete = vi.fn()
|
||||
|
|
@ -67,18 +119,33 @@ describe("SkillItem", () => {
|
|||
expect(screen.getByText("A test skill description")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode badge when skill has mode", () => {
|
||||
render(<SkillItem skill={mockSkillWithMode} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
expect(screen.getByText("architect")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not render mode badge when skill has no mode", () => {
|
||||
it("renders mode dropdown for non-built-in skills", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Should not have any mode badge
|
||||
const container = screen.getByText("test-skill").parentElement
|
||||
expect(container?.querySelector(".bg-vscode-badge-background")).toBeNull()
|
||||
expect(screen.getByTestId("select")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode dropdown with correct current value", () => {
|
||||
render(<SkillItem skill={mockSkillWithMode} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const select = screen.getByTestId("select")
|
||||
expect(select).toHaveAttribute("data-value", "architect")
|
||||
})
|
||||
|
||||
it("renders mode dropdown with __any__ for skills without mode", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const select = screen.getByTestId("select")
|
||||
expect(select).toHaveAttribute("data-value", "__any__")
|
||||
})
|
||||
|
||||
it("does not render mode dropdown for built-in skills", () => {
|
||||
render(<SkillItem skill={mockBuiltInSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Should not have a select element
|
||||
expect(screen.queryByTestId("select")).not.toBeInTheDocument()
|
||||
// Should have a static badge instead
|
||||
expect(screen.getByText("Any mode")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onEdit when edit button is clicked", () => {
|
||||
|
|
@ -91,16 +158,24 @@ describe("SkillItem", () => {
|
|||
expect(mockOnEdit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("calls onDelete when delete button is clicked", () => {
|
||||
it("calls onDelete when delete button is clicked for non-built-in skills", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
// Second button is delete
|
||||
// Find the delete button (second one for non-built-in)
|
||||
fireEvent.click(buttons[1])
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("does not render delete button for built-in skills", () => {
|
||||
render(<SkillItem skill={mockBuiltInSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Should only have 1 button (edit) for built-in skills
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
expect(buttons).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("calls onEdit when clicking on skill name area", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
|
|
@ -110,6 +185,38 @@ describe("SkillItem", () => {
|
|||
expect(mockOnEdit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("sends moveSkill message when mode is changed", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Simulate mode change
|
||||
const changeButton = screen.getByTestId("select-change-button")
|
||||
fireEvent.click(changeButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "moveSkill",
|
||||
skillName: "test-skill",
|
||||
source: "project",
|
||||
skillMode: undefined,
|
||||
newSkillMode: "code",
|
||||
})
|
||||
})
|
||||
|
||||
it("sends moveSkill message with correct current mode", () => {
|
||||
render(<SkillItem skill={mockSkillWithMode} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Simulate mode change
|
||||
const changeButton = screen.getByTestId("select-change-button")
|
||||
fireEvent.click(changeButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "moveSkill",
|
||||
skillName: "mode-specific-skill",
|
||||
source: "global",
|
||||
skillMode: "architect",
|
||||
newSkillMode: "code",
|
||||
})
|
||||
})
|
||||
|
||||
it("renders without description when not provided", () => {
|
||||
const skillWithoutDescription: SkillMetadata = {
|
||||
name: "no-desc-skill",
|
||||
|
|
@ -132,10 +239,19 @@ describe("SkillItem", () => {
|
|||
expect(itemDiv).toHaveClass("hover:bg-vscode-list-hoverBackground")
|
||||
})
|
||||
|
||||
it("renders both edit and delete buttons", () => {
|
||||
it("renders edit and delete buttons for non-built-in skills", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
const buttons = screen.getAllByTestId("button")
|
||||
expect(buttons).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("includes available modes in the dropdown", () => {
|
||||
render(<SkillItem skill={mockSkill} onEdit={mockOnEdit} onDelete={mockOnDelete} />)
|
||||
|
||||
// Check that select items are rendered
|
||||
const selectItems = screen.getAllByTestId("select-item")
|
||||
// Should have "Any mode" + 3 modes (code, architect, ask)
|
||||
expect(selectItems).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ca/settings.json
generated
2
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -1003,6 +1003,8 @@
|
|||
"addSkill": "Afegir Skill",
|
||||
"editSkill": "Editar skill",
|
||||
"deleteSkill": "Eliminar skill",
|
||||
"changeMode": "Canvia mode",
|
||||
"modeAny": "Qualsevol mode",
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar Skill",
|
||||
"description": "Estàs segur que vols eliminar la skill \"{{name}}\"? Aquesta acció no es pot desfer.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/de/settings.json
generated
2
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -1003,6 +1003,8 @@
|
|||
"addSkill": "Skill hinzufügen",
|
||||
"editSkill": "Skill bearbeiten",
|
||||
"deleteSkill": "Skill löschen",
|
||||
"changeMode": "Modus ändern",
|
||||
"modeAny": "Beliebiger Modus",
|
||||
"deleteDialog": {
|
||||
"title": "Skill löschen",
|
||||
"description": "Sind Sie sicher, dass Sie die Skill \"{{name}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@
|
|||
"addSkill": "Add Skill",
|
||||
"editSkill": "Edit skill",
|
||||
"deleteSkill": "Delete skill",
|
||||
"changeMode": "Change mode",
|
||||
"modeAny": "Any mode",
|
||||
"deleteDialog": {
|
||||
"title": "Delete Skill",
|
||||
"description": "Are you sure you want to delete the skill \"{{name}}\"? This action cannot be undone.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/es/settings.json
generated
2
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -1003,6 +1003,8 @@
|
|||
"addSkill": "Añadir Skill",
|
||||
"editSkill": "Editar skill",
|
||||
"deleteSkill": "Eliminar skill",
|
||||
"changeMode": "Cambiar modo",
|
||||
"modeAny": "Cualquier modo",
|
||||
"deleteDialog": {
|
||||
"title": "Eliminar Skill",
|
||||
"description": "¿Estás seguro de que quieres eliminar la skill \"{{name}}\"? Esta acción no se puede deshacer.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/fr/settings.json
generated
2
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -1003,6 +1003,8 @@
|
|||
"addSkill": "Ajouter une Skill",
|
||||
"editSkill": "Modifier la skill",
|
||||
"deleteSkill": "Supprimer la skill",
|
||||
"changeMode": "Changer de mode",
|
||||
"modeAny": "N'importe quel mode",
|
||||
"deleteDialog": {
|
||||
"title": "Supprimer la Skill",
|
||||
"description": "Êtes-vous sûr de vouloir supprimer la skill \"{{name}}\" ? Cette action ne peut pas être annulée.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/hi/settings.json
generated
2
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Skill जोड़ें",
|
||||
"editSkill": "Skill संपादित करें",
|
||||
"deleteSkill": "Skill हटाएं",
|
||||
"changeMode": "मोड बदलें",
|
||||
"modeAny": "कोई भी मोड",
|
||||
"deleteDialog": {
|
||||
"title": "Skill हटाएं",
|
||||
"description": "क्या आप वाकई skill \"{{name}}\" को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/id/settings.json
generated
2
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -1033,6 +1033,8 @@
|
|||
"addSkill": "Tambahkan Skill",
|
||||
"editSkill": "Edit skill",
|
||||
"deleteSkill": "Hapus skill",
|
||||
"changeMode": "Ubah mode",
|
||||
"modeAny": "Mode apa saja",
|
||||
"deleteDialog": {
|
||||
"title": "Hapus Skill",
|
||||
"description": "Apakah Anda yakin ingin menghapus skill \"{{name}}\"? Tindakan ini tidak dapat dibatalkan.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/it/settings.json
generated
2
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Aggiungi Skill",
|
||||
"editSkill": "Modifica skill",
|
||||
"deleteSkill": "Elimina skill",
|
||||
"changeMode": "Cambia modalità",
|
||||
"modeAny": "Qualsiasi modalità",
|
||||
"deleteDialog": {
|
||||
"title": "Elimina Skill",
|
||||
"description": "Sei sicuro di voler eliminare la skill \"{{name}}\"? Questa azione non può essere annullata.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ja/settings.json
generated
2
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "スキルを追加",
|
||||
"editSkill": "スキルを編集",
|
||||
"deleteSkill": "スキルを削除",
|
||||
"changeMode": "モードを変更",
|
||||
"modeAny": "任意のモード",
|
||||
"deleteDialog": {
|
||||
"title": "スキルを削除",
|
||||
"description": "スキル「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ko/settings.json
generated
2
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "스킬 추가",
|
||||
"editSkill": "스킬 편집",
|
||||
"deleteSkill": "스킬 삭제",
|
||||
"changeMode": "모드 변경",
|
||||
"modeAny": "모든 모드",
|
||||
"deleteDialog": {
|
||||
"title": "스킬 삭제",
|
||||
"description": "스킬 \"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/nl/settings.json
generated
2
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Skill toevoegen",
|
||||
"editSkill": "Skill bewerken",
|
||||
"deleteSkill": "Skill verwijderen",
|
||||
"changeMode": "Modus wijzigen",
|
||||
"modeAny": "Elke modus",
|
||||
"deleteDialog": {
|
||||
"title": "Skill verwijderen",
|
||||
"description": "Weet u zeker dat u de skill \"{{name}}\" wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pl/settings.json
generated
2
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Dodaj Umiejętność",
|
||||
"editSkill": "Edytuj umiejętność",
|
||||
"deleteSkill": "Usuń umiejętność",
|
||||
"changeMode": "Zmień tryb",
|
||||
"modeAny": "Dowolny tryb",
|
||||
"deleteDialog": {
|
||||
"title": "Usuń Umiejętność",
|
||||
"description": "Czy na pewno chcesz usunąć umiejętność \"{{name}}\"? Tej akcji nie można cofnąć.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
2
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Adicionar Skill",
|
||||
"editSkill": "Editar skill",
|
||||
"deleteSkill": "Excluir skill",
|
||||
"changeMode": "Alterar modo",
|
||||
"modeAny": "Qualquer modo",
|
||||
"deleteDialog": {
|
||||
"title": "Excluir Skill",
|
||||
"description": "Tem certeza de que deseja excluir a skill \"{{name}}\"? Esta ação não pode ser desfeita.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ru/settings.json
generated
2
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Добавить Навык",
|
||||
"editSkill": "Редактировать навык",
|
||||
"deleteSkill": "Удалить навык",
|
||||
"changeMode": "Изменить режим",
|
||||
"modeAny": "Любой режим",
|
||||
"deleteDialog": {
|
||||
"title": "Удалить Навык",
|
||||
"description": "Вы уверены, что хотите удалить навык \"{{name}}\"? Это действие нельзя отменить.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/tr/settings.json
generated
2
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Beceri Ekle",
|
||||
"editSkill": "Beceriyi düzenle",
|
||||
"deleteSkill": "Beceriyi sil",
|
||||
"changeMode": "Modu değiştir",
|
||||
"modeAny": "Herhangi bir mod",
|
||||
"deleteDialog": {
|
||||
"title": "Beceriyi Sil",
|
||||
"description": "\"{{name}}\" becerisini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/vi/settings.json
generated
2
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "Thêm Skill",
|
||||
"editSkill": "Chỉnh sửa skill",
|
||||
"deleteSkill": "Xóa skill",
|
||||
"changeMode": "Thay đổi chế độ",
|
||||
"modeAny": "Bất kỳ chế độ nào",
|
||||
"deleteDialog": {
|
||||
"title": "Xóa Skill",
|
||||
"description": "Bạn có chắc chắn muốn xóa skill \"{{name}}\" không? Hành động này không thể hoàn tác.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
2
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -1004,6 +1004,8 @@
|
|||
"addSkill": "添加技能",
|
||||
"editSkill": "编辑技能",
|
||||
"deleteSkill": "删除技能",
|
||||
"changeMode": "更改模式",
|
||||
"modeAny": "任意模式",
|
||||
"deleteDialog": {
|
||||
"title": "删除技能",
|
||||
"description": "您确定要删除技能\"{{name}}\"吗?此操作无法撤销。",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
2
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -1011,6 +1011,8 @@
|
|||
"addSkill": "新增技能",
|
||||
"editSkill": "編輯技能",
|
||||
"deleteSkill": "刪除技能",
|
||||
"changeMode": "變更模式",
|
||||
"modeAny": "任意模式",
|
||||
"deleteDialog": {
|
||||
"title": "刪除技能",
|
||||
"description": "您確定要刪除技能「{{name}}」嗎?此動作無法復原。",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue