diff --git a/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx b/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx index b6c7c2dbb..5c13d0818 100644 --- a/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx +++ b/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx @@ -465,6 +465,66 @@ describe("RuntimeLocalSkillImportPanel", () => { expect(await screen.findByText("Updated")).toBeInTheDocument(); }); + it("applies a single creator conflict when clicking overwrite", async () => { + mockResolveRuntimeLocalSkillImport + .mockResolvedValueOnce({ + status: "conflict", + conflict: { + existing_skill_id: "existing-skill-1", + existing_created_by: "user-1", + can_overwrite: true, + }, + }) + .mockResolvedValueOnce({ + status: "updated", + skill: { + ...MOCK_IMPORTED_SKILL_A, + id: "existing-skill-1", + }, + }); + + renderPanel(); + + expect( + await screen.findByText("Review Helper", {}, { timeout: 5000 }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /Review Helper/i })); + + const importButton = screen.getByRole("button", { + name: /Import to Workspace/i, + }); + await waitFor(() => expect(importButton).not.toBeDisabled(), { + timeout: 5000, + }); + fireEvent.click(importButton); + + expect( + await screen.findByText(/A skill with this name already exists/i), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /^Overwrite$/i })); + + await waitFor( + () => { + expect(mockResolveRuntimeLocalSkillImport).toHaveBeenLastCalledWith( + "runtime-1", + { + skill_key: "review-helper", + name: "Review Helper", + description: "Review pull requests", + supports_conflict: true, + action: "overwrite", + target_skill_id: "existing-skill-1", + }, + ); + }, + { timeout: 5000 }, + ); + + expect(await screen.findByText("Updated")).toBeInTheDocument(); + }); + it("keeps bulk completion behavior when conflict resolution leaves one success", async () => { mockRuntimeLocalSkillsOptions.mockReturnValue({ queryKey: ["runtimes", "local-skills", "runtime-1"], diff --git a/packages/views/skills/components/runtime-local-skill-import-panel.tsx b/packages/views/skills/components/runtime-local-skill-import-panel.tsx index 799b29079..9e2deb5d9 100644 --- a/packages/views/skills/components/runtime-local-skill-import-panel.tsx +++ b/packages/views/skills/components/runtime-local-skill-import-panel.tsx @@ -307,12 +307,14 @@ function ConflictResolutionPanel({ conflicts, resolutions, onChange, + onResolveNow, onOverwriteAll, onSkipAll, }: { conflicts: BulkImportResult[]; resolutions: Record; onChange: (key: string, next: ConflictResolutionState) => void; + onResolveNow?: (key: string, next: ConflictResolutionState) => void; onOverwriteAll: () => void; onSkipAll: () => void; }) { @@ -401,12 +403,17 @@ function ConflictResolutionPanel({ variant={ resolution.action === "overwrite" ? "default" : "outline" } - onClick={() => - onChange(r.key, { + onClick={() => { + const next = { action: "overwrite", renameName: resolution.renameName, - }) - } + } satisfies ConflictResolutionState; + if (single && r.conflict?.can_overwrite && onResolveNow) { + onResolveNow(r.key, next); + } else { + onChange(r.key, next); + } + }} disabled={!r.conflict?.can_overwrite} > @@ -723,7 +730,9 @@ export function RuntimeLocalSkillImportPanel({ })); }; - const handleApplyConflictResolutions = async () => { + const handleApplyConflictResolutions = async ( + resolutionOverrides: Record = {}, + ) => { if (!selectedRuntimeId || pendingConflicts.length === 0) return; const conflicts = [...pendingConflicts]; @@ -748,7 +757,8 @@ export function RuntimeLocalSkillImportPanel({ })); for (const r of conflicts) { - const resolution = conflictResolutions[r.key]; + const resolution = + resolutionOverrides[r.key] ?? conflictResolutions[r.key]; if (!resolution) { applyResult(r.key, { status: "failed", @@ -893,6 +903,10 @@ export function RuntimeLocalSkillImportPanel({ conflicts={pendingConflicts} resolutions={conflictResolutions} onChange={setConflictResolution} + onResolveNow={(key, next) => { + setConflictResolution(key, next); + void handleApplyConflictResolutions({ [key]: next }); + }} onOverwriteAll={() => { setConflictResolutions((prev) => { const next = { ...prev }; @@ -1143,7 +1157,7 @@ export function RuntimeLocalSkillImportPanel({