mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(skills): reconcile stale share selection
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user