fix(skills): harden rename to managed roots and cover failures
Restrict in-place skill rename to managed skill directories, require frontmatter name to match before moving, roll back/reject with tests, hide rename in the UI for unmanaged paths, and drop unused toast keys. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
f0591515fd
commit
bfea13ef1d
@@ -27,6 +27,7 @@ import { SidebarGroup } from '@/components/sections/shared/SidebarGroup';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { SETTINGS_PANEL_TITLE_CLASS } from '@/components/sections/shared/SettingsSection';
|
||||
import { isManagedSkillFilesystemPath } from '@/components/sections/skills/skillLocations';
|
||||
|
||||
interface SkillsSidebarProps {
|
||||
onItemSelect?: () => void;
|
||||
@@ -35,6 +36,9 @@ interface SkillsSidebarProps {
|
||||
const BUILT_IN_SKILL_LOCATION = '<built-in>';
|
||||
|
||||
const isBuiltInSkill = (skill: DiscoveredSkill | null | undefined): boolean => skill?.path === BUILT_IN_SKILL_LOCATION;
|
||||
const isRenamableSkill = (skill: DiscoveredSkill | null | undefined): boolean => (
|
||||
!!skill && !isBuiltInSkill(skill) && isManagedSkillFilesystemPath(skill.path)
|
||||
);
|
||||
|
||||
export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) => {
|
||||
const { t } = useI18n();
|
||||
@@ -140,14 +144,14 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
|
||||
};
|
||||
|
||||
const handleOpenRenameDialog = (skill: DiscoveredSkill) => {
|
||||
if (isBuiltInSkill(skill)) return;
|
||||
if (!isRenamableSkill(skill)) return;
|
||||
setRenameNewName(skill.name);
|
||||
setRenameDialogSkill(skill);
|
||||
};
|
||||
|
||||
const handleRenameSkill = async () => {
|
||||
if (!renameDialogSkill) return;
|
||||
if (isBuiltInSkill(renameDialogSkill)) {
|
||||
if (!isRenamableSkill(renameDialogSkill)) {
|
||||
setRenameDialogSkill(null);
|
||||
return;
|
||||
}
|
||||
@@ -443,13 +447,16 @@ const SkillListItem: React.FC<SkillListItemProps> = ({
|
||||
: t('settings.skills.sidebar.badge.opencode');
|
||||
const badgeClassName = 'typography-micro text-muted-foreground bg-[var(--surface-muted)] px-1 rounded flex-shrink-0 leading-none pb-px border border-[var(--interactive-border)]/50';
|
||||
const isBuiltIn = isBuiltInSkill(skill);
|
||||
const canRename = isRenamableSkill(skill);
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false);
|
||||
const renderMenuItems = (Item: React.ElementType) => (
|
||||
<>
|
||||
<Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRename(); }}>
|
||||
<Icon name="edit" className="h-4 w-4 mr-px" />
|
||||
{t('settings.common.actions.rename')}
|
||||
</Item>
|
||||
{canRename ? (
|
||||
<Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRename(); }}>
|
||||
<Icon name="edit" className="h-4 w-4 mr-px" />
|
||||
{t('settings.common.actions.rename')}
|
||||
</Item>
|
||||
) : null}
|
||||
<Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onDuplicate(); }}>
|
||||
<Icon name="file-copy" className="h-4 w-4 mr-px" />
|
||||
{t('settings.common.actions.duplicate')}
|
||||
|
||||
@@ -57,3 +57,22 @@ export function locationPartsFrom(value: SkillLocationValue): { scope: SkillScop
|
||||
}
|
||||
return { scope: match.scope, source: match.source };
|
||||
}
|
||||
|
||||
/** True when a discovered skill path is under a managed skill root that rename/delete may mutate. */
|
||||
export function isManagedSkillFilesystemPath(skillPath: string | null | undefined): boolean {
|
||||
if (!skillPath || skillPath === '<built-in>') return false;
|
||||
const normalized = skillPath.replace(/\\/g, '/');
|
||||
if (
|
||||
normalized.includes('/.cache/opencode/skills/')
|
||||
|| normalized.includes('/Caches/opencode/skills/')
|
||||
|| normalized.includes('/Library/Caches/opencode/skills/')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/\/\.opencode\/skills?\//.test(normalized)
|
||||
|| /\/\.claude\/skills\//.test(normalized)
|
||||
|| /\/\.agents\/skills\//.test(normalized)
|
||||
|| /\/\.config\/opencode\/skills?\//.test(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -666,8 +666,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'Skill "{name}" erfolgreich gelöscht',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': 'Skill konnte nicht gelöscht werden',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': 'Skill-Details für Duplizierung konnten nicht geladen werden',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': 'Skill-Details konnten nicht geladen werden',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': 'Alter Skill konnte nach Umbenennung nicht entfernt werden',
|
||||
'settings.skills.sidebar.toast.renameFailed': 'Skill konnte nicht umbenannt werden',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'Skill umbenannt in "{name}"',
|
||||
'settings.skills.sidebar.deleteDialog.title': 'Skill löschen',
|
||||
|
||||
@@ -718,8 +718,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'Skill "{name}" deleted successfully',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': 'Failed to delete skill',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': 'Failed to load skill details for duplication',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': 'Failed to load skill details',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': 'Failed to remove old skill after rename',
|
||||
'settings.skills.sidebar.toast.renameFailed': 'Failed to rename skill',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'Skill renamed to "{name}"',
|
||||
'settings.skills.sidebar.deleteDialog.title': 'Delete Skill',
|
||||
|
||||
@@ -685,8 +685,6 @@ export const settingsDict = {
|
||||
"settings.skills.sidebar.toast.skillDeleted": "Habilidad \"{name}\" eliminada con éxito",
|
||||
"settings.skills.sidebar.toast.deleteSkillFailed": "No se pudo eliminar la habilidad",
|
||||
"settings.skills.sidebar.toast.duplicateLoadFailed": "No se pudo cargar la información de la habilidad para duplicarla",
|
||||
"settings.skills.sidebar.toast.renameLoadFailed": "No se pudo cargar la información de la habilidad",
|
||||
"settings.skills.sidebar.toast.removeOldAfterRenameFailed": "No se pudo eliminar la habilidad antigua después del cambio de nombre",
|
||||
"settings.skills.sidebar.toast.renameFailed": "No se pudo cambiar el nombre de la habilidad",
|
||||
"settings.skills.sidebar.toast.skillRenamed": "Habilidad renombrada a \"{name}\"",
|
||||
"settings.skills.sidebar.deleteDialog.title": "Eliminar habilidad",
|
||||
|
||||
@@ -606,8 +606,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'Skill "{name}" supprimé avec succès',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': 'Échec de la suppression du skill',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': 'Échec du chargement des détails du skill pour la duplication',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': 'Échec du chargement des détails du skill',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': 'Échec de la suppression de l\'ancien skill après le renommage',
|
||||
'settings.skills.sidebar.toast.renameFailed': 'Échec du renommage du skill',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'Skill renommé en "{name}"',
|
||||
'settings.skills.sidebar.deleteDialog.title': 'Supprimer le skill',
|
||||
|
||||
@@ -718,8 +718,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'Skill "{name}" を削除しました',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': 'Skill の削除に失敗しました',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': '複製用の Skill 詳細の読み込みに失敗しました',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': 'Skill 詳細の読み込みに失敗しました',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': '名前変更後に古い Skill の削除に失敗しました',
|
||||
'settings.skills.sidebar.toast.renameFailed': 'Skill の名前変更に失敗しました',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'Skill の名前を「{name}」に変更しました',
|
||||
'settings.skills.sidebar.deleteDialog.title': 'Skill を削除',
|
||||
|
||||
@@ -685,8 +685,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': '스킬 "{name}"을 삭제했습니다',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': '스킬을 삭제하지 못했습니다',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': '복제를 위한 스킬 세부 정보를 로드하지 못했습니다',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': '스킬 세부 정보를 로드하지 못했습니다',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': '이름 변경 후 이전 스킬을 제거하지 못했습니다',
|
||||
'settings.skills.sidebar.toast.renameFailed': '스킬 이름을 변경하지 못했습니다',
|
||||
'settings.skills.sidebar.toast.skillRenamed': '스킬 이름이 "{name}"(으)로 변경되었습니다',
|
||||
'settings.skills.sidebar.deleteDialog.title': '스킬 삭제',
|
||||
|
||||
@@ -1863,9 +1863,7 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.title': 'Umiejętności',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': 'Nie udało się usunąć umiejętności',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': 'Nie udało się załadować szczegółów umiejętności do duplikacji',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': 'Nie udało się usunąć starej umiejętności po zmianie nazwy',
|
||||
'settings.skills.sidebar.toast.renameFailed': 'Nie udało się zmienić nazwy umiejętności',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': 'Nie udało się załadować szczegółów umiejętności',
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'Umiejętność „{name}” została usunięta',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'Zmieniono nazwę umiejętności na „{name}”',
|
||||
'settings.skills.sidebar.total': 'Suma: {count}',
|
||||
|
||||
@@ -685,8 +685,6 @@ export const settingsDict = {
|
||||
"settings.skills.sidebar.toast.skillDeleted": "Habilidade \"{name}\" excluída com sucesso",
|
||||
"settings.skills.sidebar.toast.deleteSkillFailed": "Não foi possível excluir a habilidade",
|
||||
"settings.skills.sidebar.toast.duplicateLoadFailed": "Não foi possível carregar as informações da habilidade para duplicá-la",
|
||||
"settings.skills.sidebar.toast.renameLoadFailed": "Não foi possível carregar as informações da habilidade",
|
||||
"settings.skills.sidebar.toast.removeOldAfterRenameFailed": "Não foi possível excluir a habilidade antiga depois da renomeação",
|
||||
"settings.skills.sidebar.toast.renameFailed": "Não foi possível renomear da habilidade",
|
||||
"settings.skills.sidebar.toast.skillRenamed": "Habilidade renomeada para \"{name}\"",
|
||||
"settings.skills.sidebar.deleteDialog.title": "Excluir habilidade",
|
||||
|
||||
@@ -685,8 +685,6 @@ export const settingsDict = {
|
||||
"settings.skills.sidebar.toast.skillDeleted": "Навичку \"{name}\" успішно видалено",
|
||||
"settings.skills.sidebar.toast.deleteSkillFailed": "Не вдалося видалити навичку",
|
||||
"settings.skills.sidebar.toast.duplicateLoadFailed": "Не вдалося завантажити деталі навичок для дублювання",
|
||||
"settings.skills.sidebar.toast.renameLoadFailed": "Не вдалося завантажити деталі навичок",
|
||||
"settings.skills.sidebar.toast.removeOldAfterRenameFailed": "Не вдалося видалити стару навичку після перейменування",
|
||||
"settings.skills.sidebar.toast.renameFailed": "Не вдалося перейменувати навичку",
|
||||
"settings.skills.sidebar.toast.skillRenamed": "Навичку перейменовано на \"{name}\"",
|
||||
"settings.skills.sidebar.deleteDialog.title": "Видалити навичку",
|
||||
|
||||
@@ -685,8 +685,6 @@ export const settingsDict = {
|
||||
'settings.skills.sidebar.toast.skillDeleted': '技能“{name}”已删除',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': '删除技能失败',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': '加载技能详情以复制失败',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': '加载技能详情失败',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': '重命名后移除旧技能失败',
|
||||
'settings.skills.sidebar.toast.renameFailed': '重命名技能失败',
|
||||
'settings.skills.sidebar.toast.skillRenamed': '技能已重命名为“{name}”',
|
||||
'settings.skills.sidebar.deleteDialog.title': '删除技能',
|
||||
|
||||
@@ -682,8 +682,6 @@
|
||||
'settings.skills.sidebar.toast.skillDeleted': 'skill「{name}」已刪除',
|
||||
'settings.skills.sidebar.toast.deleteSkillFailed': '刪除 skill 失敗',
|
||||
'settings.skills.sidebar.toast.duplicateLoadFailed': '複製 skill 的詳細資訊載入失敗',
|
||||
'settings.skills.sidebar.toast.renameLoadFailed': '載入 skill 詳情失敗',
|
||||
'settings.skills.sidebar.toast.removeOldAfterRenameFailed': '重新命名後移除舊 skill 失敗',
|
||||
'settings.skills.sidebar.toast.renameFailed': '重新命名 skill 失敗',
|
||||
'settings.skills.sidebar.toast.skillRenamed': 'skill 已重新命名為「{name}」',
|
||||
'settings.skills.sidebar.deleteDialog.title': '刪除 Skill',
|
||||
|
||||
Reference in New Issue
Block a user