fix(skills): reconcile stale share selection

This commit is contained in:
Jinwoo-H
2026-08-12 08:13:00 -04:00
parent b2e79e2150
commit 4115726e19
4 changed files with 56 additions and 7 deletions
@@ -542,12 +542,13 @@ does not mean the surrounding phase is complete.
installations.
- [x] Detect an agent before creating its provider-specific configuration root.
- [x] Avoid modifying roots for agents that consume `.agents/skills` directly.
- [ ] Keep loose-skill placement as the universal/default installation path.
- [ ] Use the portable root directly for compatible Cursor installations.
- [ ] Generate validated `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` adapters only
for detected clients where native plugin installation is useful.
- [ ] Fall back to loose skills for unsupported plugin clients, including the current Codex IDE
extension, and retain bundle/version provenance for every installed skill.
- [x] Keep loose-skill placement as the universal/default installation path.
- [x] Do not add Cursor-specific placement in V1; keep the Agent Plugins-compatible root as the
portable archive shape while installing contained skills through canonical loose-skill paths.
- [x] Do not generate `.claude-plugin/plugin.json` or `.codex-plugin/plugin.json` adapters in V1;
native plugin installation remains a separately reviewed follow-up if it becomes useful.
- [x] Use loose skills for plugin clients, including the current Codex IDE extension, and retain
bundle/version provenance for every installed skill.
### Placement reconciliation
@@ -886,7 +887,10 @@ does not mean the surrounding phase is complete.
### UX validation
- [ ] Test keyboard shortcuts and labels with platform-aware macOS versus Windows/Linux behavior.
- [ ] Test loading, error, cancellation, partial, conflict, and stale-preview states.
- [x] Test loading, error, cancellation, partial, conflict, and stale-preview states. Renderer
coverage includes scan loading, unavailable links, upload/install cancellation, partial
provider results, keep-local conflicts, capability loss after preview, and selection
reconciliation after a changed discovery scan; package tests cover source drift.
- [ ] Test screen-reader names, focus order, progress announcements, and destructive confirmations.
- [ ] Test long names, release notes, paths, organization names, and localized copy.
- [ ] Review trust and privacy wording with security and design.
@@ -70,6 +70,24 @@ export function addShareableSkillResults(
return next
}
export function retainedShareableSkillSelection(
current: Set<string>,
skills: readonly DiscoveredSkill[],
local: boolean
): Set<string> {
const next = new Set<string>()
const names = new Set<string>()
for (const skill of skills) {
const name = shareSkillNameKey(skill)
if (!current.has(skill.id) || !isSkillShareEligible(skill, local) || names.has(name)) {
continue
}
next.add(skill.id)
names.add(name)
}
return next.size === current.size ? current : next
}
export function updatedSkillSelection(
current: ReadonlySet<string>,
skillId: string,
@@ -252,4 +252,26 @@ describe('SkillsPage', () => {
'A skill with this name is already selected from another source.'
)
})
it('drops stale selections when a refreshed scan no longer contains the skill', async () => {
const discover = vi
.fn()
.mockResolvedValueOnce(discoveryResult(['alpha', 'beta']))
.mockResolvedValueOnce(discoveryResult(['beta']))
Object.defineProperty(window, 'api', {
configurable: true,
value: { skills: skillsApi(discover), runtimeEnvironments: { call: vi.fn() } }
})
await renderPage()
await flushMicrotasks()
await act(async () => fireEvent.click(buttonNamed('Share skills')))
await act(async () => fireEvent.click(selectionCheckbox('alpha')))
expect(container?.textContent).toContain('1 selected')
await act(async () => fireEvent.click(buttonNamed('Refresh')))
await flushMicrotasks()
expect(container?.textContent).toContain('0 selected')
expect(renderedSkillNames()).toEqual(['beta'])
})
})
@@ -29,6 +29,7 @@ import {
SkillShareSelectionAction,
SkillShareSelectionStatus,
addShareableSkillResults,
retainedShareableSkillSelection,
updatedSkillSelection
} from './SkillShareSelectionControls'
@@ -117,8 +118,12 @@ export default function SkillsPage(): React.JSX.Element {
}
try {
const nextResult = await discoverSkillsForRuntimeTarget(runtimeTarget)
const local = runtimeTarget.kind === 'local'
if (isCurrentScan()) {
setResult(nextResult)
setSelectedSkillIds((current) =>
retainedShareableSkillSelection(current, nextResult.skills, local)
)
}
} catch (error) {
console.error('Failed to discover skills:', error)