fix(editor): Lock the skill scope for users who cannot delete the skill (no-changelog) (#41221)

This commit is contained in:
Ricardo Espinoza 2026-10-16 08:28:41 +00:00 committed by GitHub
parent f553331100
commit 1ee29b2875
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 40 additions and 10 deletions

View File

@ -3465,6 +3465,7 @@
"settings.context.skills.empty.description": "Add a skill by hand or upload a SKILL.md file. Agents in the scope of the skill can then use it.",
"settings.context.skills.modal.scope.label": "Where can this skill be added?",
"settings.context.skills.modal.scope.tooltip": "A global skill can be added to any agent. A project skill can be added to the agents of that project. An agent skill belongs to one agent.",
"settings.context.skills.modal.scope.noDelete": "You need permission to delete this skill to move it, because moving removes it from where it is now.",
"settings.context.skills.modal.scope.inUse": "Used by {count} agent. Remove the skill from it to change where it is available. | Used by {count} agents. Remove the skill from them to change where it is available.",
"settings.context.skills.delete.inUse": "Used by {count} agent. Remove the skill from the agent first. | Used by {count} agents. Remove the skill from each agent first.",
"settings.context.skills.delete.confirm.title": "Delete skill?",

View File

@ -335,6 +335,22 @@ describe('SettingsSkillsView', () => {
);
});
it('locks the scope when the user may edit but not delete the skill', async () => {
skillsStore.skills = [skill()];
skillsStore.count = 1;
skillsStore.fetchSkill.mockResolvedValue(detail({ canEdit: true, canDelete: false }));
const { getByText } = renderView();
await new Promise(process.nextTick);
await userEvent.click(getByText('research-search'));
await new Promise(process.nextTick);
expect(lastModalData().readOnly).toBe(false);
expect(lastModalData().scopeLockedReason).toBe(
'You need permission to delete this skill to move it, because moving removes it from where it is now.',
);
});
it('moves an unused skill to the scope the user picks', async () => {
skillsStore.skills = [skill()];
skillsStore.count = 1;

View File

@ -245,6 +245,28 @@ function scopeValueOf(skill: Pick<SkillListItem, 'scope' | 'projectId'>): string
return skill.scope;
}
/**
* Why the scope of a saved skill cannot change, if it cannot. The backend refuses a move
* while an agent uses the skill, and a move needs the delete permission where the skill is,
* because it takes the skill out of that scope.
*/
function scopeLock(
detail: Pick<SkillListItem, 'usedByAgents' | 'canDelete'>,
): Pick<AgentSkillModalData, 'scopeLockedReason'> {
if (detail.usedByAgents > 0) {
return {
scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.inUse', {
interpolate: { count: detail.usedByAgents },
adjustToNumber: detail.usedByAgents,
}),
};
}
if (!detail.canDelete) {
return { scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.noDelete') };
}
return {};
}
async function openEditModal(item: Pick<SkillListItem, 'id'>) {
try {
const detail = await skillsStore.fetchSkill(item.id);
@ -254,22 +276,13 @@ async function openEditModal(item: Pick<SkillListItem, 'id'>) {
const options = scopeOptions.value.some((option) => option.value === currentValue)
? scopeOptions.value
: [{ value: currentValue, ...scopeDisplay(detail) }, ...scopeOptions.value];
// The backend refuses to move a skill while an agent uses it.
const inUse = detail.usedByAgents > 0;
openModal({
skill: detail.skill,
skillId: detail.id,
readOnly: !detail.canEdit,
scopeOptions: options,
initialScope: currentValue,
...(inUse
? {
scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.inUse', {
interpolate: { count: detail.usedByAgents },
adjustToNumber: detail.usedByAgents,
}),
}
: {}),
...scopeLock(detail),
existingSkillNames: [],
onConfirm: async ({ skill, scope }) =>
await onSave(