From 2201b54f2852c6f91c634f0a8ccd319bc10f5882 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 16 Apr 2026 23:11:13 +0300 Subject: [PATCH] fix(extensions): reset stale skill review state on close --- .../extensions/skills/SkillEditorDialog.tsx | 12 +++ .../extensions/skills/SkillImportDialog.tsx | 12 +++ .../skills/SkillEditorDialog.test.ts | 73 ++++++++++++++++- .../skills/SkillImportDialog.test.ts | 78 ++++++++++++++++++- 4 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx index bf2e9f1d..6ed48334 100644 --- a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +++ b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx @@ -219,6 +219,18 @@ export const SkillEditorDialog = ({ setMutationError(null); }, [detail, mode, open, projectPath]); + useEffect(() => { + if (open) { + return; + } + + setReviewPreview(null); + setReviewOpen(false); + setReviewLoading(false); + setSaveLoading(false); + setMutationError(null); + }, [open]); + useEffect(() => { if (open && mode === 'create' && scope === 'project' && !projectPath) { setScope('user'); diff --git a/src/renderer/components/extensions/skills/SkillImportDialog.tsx b/src/renderer/components/extensions/skills/SkillImportDialog.tsx index c9ac3360..eaf7a038 100644 --- a/src/renderer/components/extensions/skills/SkillImportDialog.tsx +++ b/src/renderer/components/extensions/skills/SkillImportDialog.tsx @@ -93,6 +93,18 @@ export const SkillImportDialog = ({ setMutationError(null); }, [open, projectPath]); + useEffect(() => { + if (open) { + return; + } + + setPreview(null); + setReviewOpen(false); + setReviewLoading(false); + setImportLoading(false); + setMutationError(null); + }, [open]); + useEffect(() => { if (!open || folderNameEdited) { return; diff --git a/test/renderer/components/extensions/skills/SkillEditorDialog.test.ts b/test/renderer/components/extensions/skills/SkillEditorDialog.test.ts index 72cce57a..1f081ae3 100644 --- a/test/renderer/components/extensions/skills/SkillEditorDialog.test.ts +++ b/test/renderer/components/extensions/skills/SkillEditorDialog.test.ts @@ -132,7 +132,8 @@ vi.mock('@renderer/components/extensions/skills/SkillCodeEditor', () => ({ })); vi.mock('@renderer/components/extensions/skills/SkillReviewDialog', () => ({ - SkillReviewDialog: () => null, + SkillReviewDialog: ({ open }: { open: boolean }) => + open ? React.createElement('div', { 'data-testid': 'skill-review-dialog' }, 'Review') : null, })); vi.mock('lucide-react', () => { @@ -242,4 +243,74 @@ This file uses a freeform layout without generated sections. await Promise.resolve(); }); }); + + it('clears review state when the editor closes externally', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + storeState.previewSkillUpsert.mockResolvedValue({ + planId: 'plan-1', + targetSkillDir: '/tmp/project/.claude/skills/new-skill', + changes: [ + { + relativePath: 'SKILL.md', + absolutePath: '/tmp/project/.claude/skills/new-skill/SKILL.md', + action: 'create', + oldContent: null, + newContent: '# Skill', + isBinary: false, + }, + ], + warnings: [], + summary: { created: 1, updated: 0, deleted: 0, binary: 0 }, + }); + + await act(async () => { + root.render( + React.createElement(SkillEditorDialog, { + open: true, + mode: 'create', + projectPath: '/tmp/project', + projectLabel: 'Project', + detail: null, + onClose: vi.fn(), + onSaved: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + const reviewButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Review And Create') + ) as HTMLButtonElement; + await act(async () => { + reviewButton.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="skill-review-dialog"]')).not.toBeNull(); + + await act(async () => { + root.render( + React.createElement(SkillEditorDialog, { + open: false, + mode: 'create', + projectPath: '/tmp/project', + projectLabel: 'Project', + detail: null, + onClose: vi.fn(), + onSaved: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="skill-review-dialog"]')).toBeNull(); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); }); diff --git a/test/renderer/components/extensions/skills/SkillImportDialog.test.ts b/test/renderer/components/extensions/skills/SkillImportDialog.test.ts index 38d23c20..b52cd66b 100644 --- a/test/renderer/components/extensions/skills/SkillImportDialog.test.ts +++ b/test/renderer/components/extensions/skills/SkillImportDialog.test.ts @@ -93,7 +93,8 @@ vi.mock('@renderer/components/ui/select', () => ({ })); vi.mock('@renderer/components/extensions/skills/SkillReviewDialog', () => ({ - SkillReviewDialog: () => null, + SkillReviewDialog: ({ open }: { open: boolean }) => + open ? React.createElement('div', { 'data-testid': 'skill-review-dialog' }, 'Review') : null, })); vi.mock('lucide-react', () => { @@ -302,4 +303,79 @@ describe('SkillImportDialog', () => { await Promise.resolve(); }); }); + + it('clears review state when the import dialog closes externally', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + storeState.previewSkillImport.mockResolvedValue({ + planId: 'plan-1', + targetSkillDir: '/tmp/imported-skill', + changes: [ + { + relativePath: 'SKILL.md', + absolutePath: '/tmp/imported-skill/SKILL.md', + action: 'create', + oldContent: null, + newContent: '# Skill', + isBinary: false, + }, + ], + warnings: [], + summary: { created: 1, updated: 0, deleted: 0, binary: 0 }, + }); + + await act(async () => { + root.render( + React.createElement(SkillImportDialog, { + open: true, + projectPath: null, + projectLabel: null, + onClose: vi.fn(), + onImported: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + const sourceInput = host.querySelector('#skill-import-source') as HTMLInputElement; + await act(async () => { + const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; + setValue?.call(sourceInput, '/tmp/source-skill'); + sourceInput.dispatchEvent(new Event('input', { bubbles: true })); + await Promise.resolve(); + }); + + const reviewButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Review And Import') + ) as HTMLButtonElement; + await act(async () => { + reviewButton.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="skill-review-dialog"]')).not.toBeNull(); + + await act(async () => { + root.render( + React.createElement(SkillImportDialog, { + open: false, + projectPath: null, + projectLabel: null, + onClose: vi.fn(), + onImported: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="skill-review-dialog"]')).toBeNull(); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); });