Merge pull request #2586 from openchamber/feat/skill-renaming-content-preservation-c1d5

fix(skills): preserve SKILL.md content when renaming
This commit is contained in:
Serhii Dziupin
2026-08-03 15:04:48 +03:00
committed by GitHub
22 changed files with 750 additions and 63 deletions
@@ -35,6 +35,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) && skill.renamable === true
);
export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) => {
const { t } = useI18n();
@@ -49,16 +52,16 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
skills,
setSelectedSkill,
setSkillDraft,
createSkill,
deleteSkill,
renameSkill,
getSkillDetail,
} = useSkillsStore(useShallow((s) => ({
selectedSkillName: s.selectedSkillName,
skills: s.skills,
setSelectedSkill: s.setSelectedSkill,
setSkillDraft: s.setSkillDraft,
createSkill: s.createSkill,
deleteSkill: s.deleteSkill,
renameSkill: s.renameSkill,
getSkillDetail: s.getSkillDetail,
})));
@@ -140,14 +143,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;
}
@@ -169,31 +172,11 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
return;
}
// Get full detail to copy
const detail = await getSkillDetail(renameDialogSkill.name);
if (!detail) {
toast.error(t('settings.skills.sidebar.toast.renameLoadFailed'));
setRenameDialogSkill(null);
return;
}
// Create new skill with new name
const success = await createSkill({
name: sanitizedName,
description: 'Renamed skill', // Will need proper description
scope: renameDialogSkill.scope,
source: renameDialogSkill.source,
});
// Rename in place on disk so SKILL.md body and supporting files are preserved.
const success = await renameSkill(renameDialogSkill.name, sanitizedName);
if (success) {
// Delete old skill
const deleteSuccess = await deleteSkill(renameDialogSkill.name);
if (deleteSuccess) {
toast.success(`Skill renamed to "${sanitizedName}"`);
setSelectedSkill(sanitizedName);
} else {
toast.error(t('settings.skills.sidebar.toast.removeOldAfterRenameFailed'));
}
toast.success(t('settings.skills.sidebar.toast.skillRenamed', { name: sanitizedName }));
setSelectedSkill(sanitizedName);
} else {
toast.error(t('settings.skills.sidebar.toast.renameFailed'));
}
@@ -463,13 +446,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')}