@@ -13,6 +13,14 @@ import { useRegistryStore } from '../stores/registryStore';
1313import type { RegistryType , RegistryEntry } from '../stores/registryStore' ;
1414import { loadInstalledExtensions } from '../extensions/loader' ;
1515
16+ // ─── Extension IPC bridge (minimal shape) ─────────────────────────────────────
17+ interface ExtensionsIpcApi {
18+ getInstalled : ( ) => Promise < Array < { id : string ; version : string } > > ;
19+ install : ( id : string , downloadUrl : string ) => Promise < { success : boolean ; error ?: string } > ;
20+ uninstall : ( id : string ) => Promise < void > ;
21+ }
22+ type ExtApiWindow = Window & { api ?: { extensions ?: ExtensionsIpcApi } } ;
23+
1624/** Returns true if `registryVer` is strictly greater than `installedVer` (semver). */
1725function isNewer ( registryVer : string | undefined , installedVer : string | undefined ) : boolean {
1826 if ( ! registryVer || ! installedVer ) return false ;
@@ -78,52 +86,56 @@ function Badge({ label }: { label: string }) {
7886
7987// ─── Type pill ────────────────────────────────────────────────────────────────
8088
81- type TypeFilter = 'all' | 'provider' | 'mcp' | 'theme' | 'persona' | 'prompt' | 'profile' ;
89+ type TypeFilter = 'all' | 'provider' | 'mcp' | 'theme' | 'persona' | 'prompt' | 'profile' | 'extension' ;
8290
8391const TYPE_PILLS : { id : TypeFilter ; label : string } [ ] = [
84- { id : 'all' , label : 'All' } ,
85- { id : 'provider' , label : 'Providers' } ,
86- { id : 'mcp' , label : 'MCP' } ,
87- { id : 'theme' , label : 'Themes' } ,
88- { id : 'persona' , label : 'Personas' } ,
89- { id : 'prompt' , label : 'Prompts' } ,
90- { id : 'profile' , label : 'Profiles' } ,
92+ { id : 'all' , label : 'All' } ,
93+ { id : 'provider' , label : 'Providers' } ,
94+ { id : 'mcp' , label : 'MCP' } ,
95+ { id : 'theme' , label : 'Themes' } ,
96+ { id : 'persona' , label : 'Personas' } ,
97+ { id : 'prompt' , label : 'Prompts' } ,
98+ { id : 'profile' , label : 'Profiles' } ,
99+ { id : 'extension' , label : 'Extensions' } ,
91100] ;
92101
93102/** Registry type → TypeFilter mapping */
94103const _REGISTRY_TYPE_MAP : Record < RegistryType , TypeFilter > = {
95- themes : 'theme' ,
96- personas : 'persona' ,
97- prompts : 'prompt' ,
98- profiles : 'profile' ,
99- providers : 'provider' ,
100- mcp : 'mcp' ,
104+ themes : 'theme' ,
105+ personas : 'persona' ,
106+ prompts : 'prompt' ,
107+ profiles : 'profile' ,
108+ providers : 'provider' ,
109+ mcp : 'mcp' ,
110+ extensions : 'extension' ,
101111} ;
102112
103113// ─── Unified entry ────────────────────────────────────────────────────────────
104114
105115type UnifiedEntry =
106- | { kind : 'provider' ; id : string ; name : string ; description : string ; badge : string ; installed : boolean ; emoji ?: string }
107- | { kind : 'mcp' ; id : string ; name : string ; description : string ; badge ?: string ; installed : boolean ; emoji : string ; notes ?: string }
108- | { kind : 'theme' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
109- | { kind : 'persona' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
110- | { kind : 'prompt' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
111- | { kind : 'profile' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry } ;
116+ | { kind : 'provider' ; id : string ; name : string ; description : string ; badge : string ; installed : boolean ; emoji ?: string }
117+ | { kind : 'mcp' ; id : string ; name : string ; description : string ; badge ?: string ; installed : boolean ; emoji : string ; notes ?: string }
118+ | { kind : 'theme' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
119+ | { kind : 'persona' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
120+ | { kind : 'prompt' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
121+ | { kind : 'profile' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry }
122+ | { kind : 'extension' ; id : string ; name : string ; description : string ; author : string ; verified : boolean ; installed : boolean ; hasUpdate : boolean ; entry : RegistryEntry } ;
112123
113124// ─── Kind colour map ──────────────────────────────────────────────────────────
114125
115126const KIND_COLORS : Record < string , string > = {
116- provider : 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300' ,
117- mcp : 'bg-teal-100 text-teal-700 dark:bg-teal-900/60 dark:text-teal-300' ,
118- theme : 'bg-pink-100 text-pink-700 dark:bg-pink-900/60 dark:text-pink-300' ,
119- persona : 'bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-300' ,
120- prompt : 'bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300' ,
121- profile : 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/60 dark:text-cyan-300' ,
127+ provider : 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300' ,
128+ mcp : 'bg-teal-100 text-teal-700 dark:bg-teal-900/60 dark:text-teal-300' ,
129+ theme : 'bg-pink-100 text-pink-700 dark:bg-pink-900/60 dark:text-pink-300' ,
130+ persona : 'bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-300' ,
131+ prompt : 'bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300' ,
132+ profile : 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/60 dark:text-cyan-300' ,
133+ extension : 'bg-orange-100 text-orange-700 dark:bg-orange-900/60 dark:text-orange-300' ,
122134} ;
123135
124136const KIND_LABELS : Record < string , string > = {
125137 provider : 'Provider' , mcp : 'MCP' , theme : 'Theme' ,
126- persona : 'Persona' , prompt : 'Prompt' , profile : 'Profile' ,
138+ persona : 'Persona' , prompt : 'Prompt' , profile : 'Profile' , extension : 'Extension' ,
127139} ;
128140
129141// ─── Verified checkmark ───────────────────────────────────────────────────────
@@ -244,6 +256,20 @@ export default function MarketplaceSidebarPanel() {
244256 const [ query , setQuery ] = useState ( '' ) ;
245257 const [ typePill , setTypePill ] = useState < TypeFilter > ( 'all' ) ;
246258 const inputRef = useRef < HTMLInputElement > ( null ) ;
259+
260+ // Installed extension ids → installed version (refreshed on mount + after install/uninstall)
261+ const [ installedExtMap , setInstalledExtMap ] = useState < Map < string , string > > ( new Map ( ) ) ;
262+
263+ const refreshInstalledExt = async ( ) => {
264+ const api = ( window as ExtApiWindow ) . api ;
265+ if ( ! api ?. extensions ?. getInstalled ) return ;
266+ try {
267+ const exts = await api . extensions . getInstalled ( ) ;
268+ setInstalledExtMap ( new Map ( exts . map ( ( e ) => [ e . id , e . version ] ) ) ) ;
269+ } catch { /* extensions not available in this context */ }
270+ } ;
271+
272+ useEffect ( ( ) => { void refreshInstalledExt ( ) ; } , [ ] ) ;
247273 const { settings, saveSettings } = useSettingsStore ( ) ;
248274
249275 // Registry stores
@@ -256,14 +282,15 @@ export default function MarketplaceSidebarPanel() {
256282 // Fetch registry data for the active tab on demand
257283 useEffect ( ( ) => {
258284 const typeMap : Partial < Record < TypeFilter , RegistryType > > = {
259- theme : 'themes' ,
260- persona : 'personas' ,
261- prompt : 'prompts' ,
262- profile : 'profiles' ,
285+ theme : 'themes' ,
286+ persona : 'personas' ,
287+ prompt : 'prompts' ,
288+ profile : 'profiles' ,
289+ extension : 'extensions' ,
263290 } ;
264291 if ( typePill === 'all' ) {
265292 // Fetch all registry types in parallel on first open
266- ( [ 'themes' , 'personas' , 'prompts' , 'profiles' ] as RegistryType [ ] ) . forEach ( ( t ) => fetchType ( t ) ) ;
293+ ( [ 'themes' , 'personas' , 'prompts' , 'profiles' , 'extensions' ] as RegistryType [ ] ) . forEach ( ( t ) => fetchType ( t ) ) ;
267294 } else {
268295 const rt = typeMap [ typePill ] ;
269296 if ( rt ) fetchType ( rt ) ;
@@ -273,7 +300,7 @@ export default function MarketplaceSidebarPanel() {
273300 // Parse @mcp / @provider / @theme / etc. prefix from raw query
274301 const PREFIX_MAP : Record < string , TypeFilter > = {
275302 mcp : 'mcp' , provider : 'provider' , theme : 'theme' ,
276- persona : 'persona' , prompt : 'prompt' , profile : 'profile' ,
303+ persona : 'persona' , prompt : 'prompt' , profile : 'profile' , extension : 'extension' ,
277304 } ;
278305 const atMatch = query . match ( / ^ @ ( \w + ) \s * / i) ;
279306 const prefixType : TypeFilter = atMatch
@@ -376,14 +403,27 @@ export default function MarketplaceSidebarPanel() {
376403 entry : e ,
377404 } ) ) ;
378405
406+ const extensionEntries : UnifiedEntry [ ] = getEntries ( 'extensions' ) . map ( ( e ) => ( {
407+ kind : 'extension' ,
408+ id : e . id ,
409+ name : e . name ,
410+ description : e . description ,
411+ author : e . author ,
412+ verified : e . verified ,
413+ installed : installedExtMap . has ( e . id ) ,
414+ hasUpdate : isNewer ( e . version , installedExtMap . get ( e . id ) ) ,
415+ entry : e ,
416+ } ) ) ;
417+
379418 let pool : UnifiedEntry [ ] =
380- effectiveType === 'provider' ? providers
381- : effectiveType === 'mcp' ? mcpServers
382- : effectiveType === 'theme' ? themes
383- : effectiveType === 'persona' ? personaEntries
384- : effectiveType === 'prompt' ? promptEntries
385- : effectiveType === 'profile' ? profileEntries
386- : [ ...providers , ...mcpServers , ...themes , ...personaEntries , ...promptEntries , ...profileEntries ] ;
419+ effectiveType === 'provider' ? providers
420+ : effectiveType === 'mcp' ? mcpServers
421+ : effectiveType === 'theme' ? themes
422+ : effectiveType === 'persona' ? personaEntries
423+ : effectiveType === 'prompt' ? promptEntries
424+ : effectiveType === 'profile' ? profileEntries
425+ : effectiveType === 'extension' ? extensionEntries
426+ : [ ...providers , ...mcpServers , ...themes , ...personaEntries , ...promptEntries , ...profileEntries , ...extensionEntries ] ;
387427
388428 if ( cleanQuery ) {
389429 pool = pool . filter (
@@ -394,7 +434,7 @@ export default function MarketplaceSidebarPanel() {
394434 }
395435
396436 return pool ;
397- } , [ settings , effectiveType , cleanQuery , getEntries , installedThemeIds , installedPersonaNames , installedTemplateNames , installedProfileNames , installedThemes , installedPersonas , installedTemplates , installedProfiles ] ) ;
437+ } , [ settings , effectiveType , cleanQuery , getEntries , installedThemeIds , installedPersonaNames , installedTemplateNames , installedProfileNames , installedThemes , installedPersonas , installedTemplates , installedProfiles , installedExtMap ] ) ;
398438
399439 const handleAdd = async ( entry : UnifiedEntry ) => {
400440 if ( ! settings ) return ;
@@ -480,14 +520,25 @@ export default function MarketplaceSidebarPanel() {
480520 taskOverrides : c . taskOverrides ,
481521 fromRegistry : true ,
482522 } ) ;
523+ } else if ( entry . kind === 'extension' ) {
524+ const c = entry . entry . content as { downloadUrl ?: string } ;
525+ if ( ! c . downloadUrl ) return ;
526+ const api = ( window as ExtApiWindow ) . api ;
527+ if ( ! api ?. extensions ?. install ) return ;
528+ const result = await api . extensions . install ( entry . id , c . downloadUrl ) ;
529+ if ( result . success ) {
530+ await refreshInstalledExt ( ) ;
531+ await loadInstalledExtensions ( ) ;
532+ }
533+ return ;
483534 }
484535
485536 // Re-run the extension loader so any newly-downloaded bundle extension
486537 // appears immediately without requiring an app restart.
487538 await loadInstalledExtensions ( ) ;
488539 } ;
489540
490- const handleRemove = ( entry : UnifiedEntry ) => {
541+ const handleRemove = async ( entry : UnifiedEntry ) => {
491542 if ( entry . kind === 'theme' ) {
492543 uninstallTheme ( entry . id ) ;
493544 } else if ( entry . kind === 'persona' ) {
@@ -498,6 +549,11 @@ export default function MarketplaceSidebarPanel() {
498549 } else if ( entry . kind === 'profile' ) {
499550 const match = installedProfiles . find ( ( p ) => p . name === entry . name ) ;
500551 if ( match ) removeProfile ( match . id ) ;
552+ } else if ( entry . kind === 'extension' ) {
553+ const api = ( window as ExtApiWindow ) . api ;
554+ if ( ! api ?. extensions ?. uninstall ) return ;
555+ await api . extensions . uninstall ( entry . id ) ;
556+ await refreshInstalledExt ( ) ;
501557 }
502558 } ;
503559
@@ -519,15 +575,20 @@ export default function MarketplaceSidebarPanel() {
519575 const existing = installedProfiles . find ( ( p ) => p . name === entry . name ) ;
520576 if ( existing ) removeProfile ( existing . id ) ;
521577 await handleAdd ( entry ) ;
578+ } else if ( entry . kind === 'extension' ) {
579+ // Uninstall old version, then re-download the new .ocx
580+ await handleRemove ( entry ) ;
581+ await handleAdd ( entry ) ;
522582 }
523583 } ;
524584
525585 // Loading/error state for the active registry type
526586 const activeRegistryType : RegistryType | null =
527- effectiveType === 'theme' ? 'themes'
528- : effectiveType === 'persona' ? 'personas'
529- : effectiveType === 'prompt' ? 'prompts'
530- : effectiveType === 'profile' ? 'profiles'
587+ effectiveType === 'theme' ? 'themes'
588+ : effectiveType === 'persona' ? 'personas'
589+ : effectiveType === 'prompt' ? 'prompts'
590+ : effectiveType === 'profile' ? 'profiles'
591+ : effectiveType === 'extension' ? 'extensions'
531592 : null ;
532593
533594 const isRegistryLoading = activeRegistryType ? ! ! regLoading [ activeRegistryType ] : false ;
0 commit comments