diff --git a/src/renderer/store/slices/extensionsSlice.ts b/src/renderer/store/slices/extensionsSlice.ts index e18437ff..9d21f833 100644 --- a/src/renderer/store/slices/extensionsSlice.ts +++ b/src/renderer/store/slices/extensionsSlice.ts @@ -237,15 +237,19 @@ export const createExtensionsSlice: StateCreator { + set((prev) => { if (requestSeq !== pluginCatalogRequestSeq) { return {}; } + const nextProjectPath = projectPath ?? null; + const isSameProjectContext = prev.pluginCatalogProjectPath === nextProjectPath; + return { + pluginCatalog: isSameProjectContext ? prev.pluginCatalog : [], pluginCatalogLoading: false, pluginCatalogError: err instanceof Error ? err.message : 'Failed to load plugins', - pluginCatalogProjectPath: projectPath ?? null, + pluginCatalogProjectPath: nextProjectPath, }; }); } finally { @@ -263,7 +267,13 @@ export const createExtensionsSlice: StateCreator { if (!api.plugins) return; const state = get(); - if (pluginId in state.pluginReadmes || state.pluginReadmeLoading[pluginId]) return; + const cachedReadme = state.pluginReadmes[pluginId]; + if ( + (cachedReadme !== undefined && cachedReadme !== null) || + state.pluginReadmeLoading[pluginId] + ) { + return; + } set((prev) => ({ pluginReadmeLoading: { ...prev.pluginReadmeLoading, [pluginId]: true }, diff --git a/test/renderer/store/extensionsSlice.test.ts b/test/renderer/store/extensionsSlice.test.ts index 278f61e1..906c89b9 100644 --- a/test/renderer/store/extensionsSlice.test.ts +++ b/test/renderer/store/extensionsSlice.test.ts @@ -167,6 +167,20 @@ describe('extensionsSlice', () => { expect(store.getState().pluginCatalogLoading).toBe(false); }); + it('clears stale catalog when a different project fetch fails', async () => { + store.setState({ + pluginCatalog: [makePlugin({ pluginId: 'project-a@m' })], + pluginCatalogProjectPath: '/tmp/project-a', + }); + (api.plugins!.getAll as ReturnType).mockRejectedValue(new Error('boom')); + + await store.getState().fetchPluginCatalog('/tmp/project-b'); + + expect(store.getState().pluginCatalog).toEqual([]); + expect(store.getState().pluginCatalogProjectPath).toBe('/tmp/project-b'); + expect(store.getState().pluginCatalogError).toBe('boom'); + }); + it('dedups concurrent requests for the same project key', async () => { let resolveFetch!: (plugins: EnrichedPlugin[]) => void; const inFlight = new Promise((resolve) => { @@ -243,6 +257,15 @@ describe('extensionsSlice', () => { expect(api.plugins!.getReadme).not.toHaveBeenCalled(); }); + + it('retries README fetch when the cached value is null', () => { + store.setState({ pluginReadmes: { 'test@m': null } }); + (api.plugins!.getReadme as ReturnType).mockResolvedValue(null); + + store.getState().fetchPluginReadme('test@m'); + + expect(api.plugins!.getReadme).toHaveBeenCalledWith('test@m'); + }); }); describe('mcpBrowse', () => {