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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user