@@ -48,6 +48,9 @@ const STATUS_STYLES = {
4848 disabled : 'bg-zinc-700 text-zinc-400' ,
4949 not_installed : 'border border-zinc-700 text-zinc-500' ,
5050 incompatible : 'bg-orange-500/20 text-orange-400' ,
51+ installing : 'bg-blue-500/20 text-blue-400' ,
52+ setting_up : 'bg-blue-500/20 text-blue-400' ,
53+ error : 'bg-red-500/20 text-red-300' ,
5154}
5255
5356export default function Extensions ( ) {
@@ -63,6 +66,8 @@ export default function Extensions() {
6366 const [ toast , setToast ] = useState ( null )
6467 const [ consoleExt , setConsoleExt ] = useState ( null )
6568 const [ refreshing , setRefreshing ] = useState ( false )
69+ const [ installProgress , setInstallProgress ] = useState ( null )
70+ const installProgressRef = useRef ( null )
6671
6772 useEffect ( ( ) => {
6873 fetchCatalog ( )
@@ -102,13 +107,30 @@ export default function Extensions() {
102107 const handleMutation = async ( serviceId , action ) => {
103108 setMutating ( serviceId )
104109 setConfirm ( null )
110+
111+ let pollInterval = null
112+ if ( action === 'install' || action === 'enable' ) {
113+ pollInterval = setInterval ( async ( ) => {
114+ try {
115+ const res = await fetchJson ( `/api/extensions/${ serviceId } /progress` )
116+ if ( res . ok ) {
117+ const data = await res . json ( )
118+ if ( data . status !== 'idle' ) {
119+ installProgressRef . current = data
120+ setInstallProgress ( data )
121+ }
122+ }
123+ } catch { /* ignore polling errors */ }
124+ } , 3000 )
125+ }
126+
105127 try {
106128 const url = action === 'uninstall'
107129 ? `/api/extensions/${ serviceId } `
108130 : `/api/extensions/${ serviceId } /${ action } `
109131 const res = await fetch ( url , {
110132 method : action === 'uninstall' ? 'DELETE' : 'POST' ,
111- signal : AbortSignal . timeout ( 120000 ) ,
133+ signal : AbortSignal . timeout ( 300000 ) ,
112134 } )
113135 if ( ! res . ok ) {
114136 const err = await res . json ( ) . catch ( ( ) => ( { } ) )
@@ -123,8 +145,14 @@ export default function Extensions() {
123145 }
124146 await fetchCatalog ( )
125147 } catch ( err ) {
126- setToast ( { type : 'error' , text : friendlyError ( err . message ) || `Failed to ${ action } extension` } )
148+ const progressError = installProgressRef . current ?. service_id === serviceId
149+ ? installProgressRef . current . error : null
150+ const base = friendlyError ( err . message ) || `Failed to ${ action } extension`
151+ setToast ( { type : 'error' , text : progressError ? `${ base } — ${ progressError } ` : base } )
127152 } finally {
153+ if ( pollInterval ) clearInterval ( pollInterval )
154+ installProgressRef . current = null
155+ setInstallProgress ( null )
128156 setMutating ( null )
129157 }
130158 }
@@ -157,8 +185,8 @@ export default function Extensions() {
157185 . filter ( Boolean )
158186 ) ]
159187
160- const STATUS_FILTERS = [ 'all' , 'enabled' , 'stopped' , 'disabled' , 'not_installed' , 'incompatible' ]
161- const STATUS_LABELS = { all : 'All' , enabled : 'Enabled' , stopped : 'Stopped' , disabled : 'Disabled' , not_installed : 'Not Installed' , incompatible : 'Incompatible' }
188+ const STATUS_FILTERS = [ 'all' , 'enabled' , 'stopped' , 'disabled' , 'installing' , 'setting_up' , 'error' , ' not_installed', 'incompatible' ]
189+ const STATUS_LABELS = { all : 'All' , enabled : 'Enabled' , stopped : 'Stopped' , disabled : 'Disabled' , installing : 'Installing' , setting_up : 'Setting Up' , error : 'Error' , not_installed : 'Not Installed' , incompatible : 'Incompatible' }
162190
163191 // Filter extensions
164192 const query = search . toLowerCase ( )
@@ -212,6 +240,8 @@ export default function Extensions() {
212240 < SummaryItem label = "Installed" value = { summary . installed ?? 0 } color = "bg-green-500" />
213241 < SummaryItem label = "Stopped" value = { summary . stopped ?? 0 } color = "bg-red-500" />
214242 < SummaryItem label = "Available" value = { summary . not_installed ?? 0 } color = "bg-indigo-500" />
243+ < SummaryItem label = "Installing" value = { summary . installing ?? 0 } color = "bg-blue-500" />
244+ < SummaryItem label = "Error" value = { summary . error ?? 0 } color = "bg-red-500" />
215245 < SummaryItem label = "Incompatible" value = { summary . incompatible ?? 0 } color = "bg-orange-500" />
216246 </ div >
217247 </ div >
@@ -286,6 +316,7 @@ export default function Extensions() {
286316 onConsole = { ( ) => setConsoleExt ( ext ) }
287317 onAction = { requestAction }
288318 mutating = { mutating }
319+ installProgress = { installProgress }
289320 />
290321 ) ) }
291322 </ div >
@@ -352,7 +383,7 @@ function SummaryItem({ label, value, color }) {
352383 )
353384}
354385
355- function ExtensionCard ( { ext, gpuBackend, agentAvailable, onDetails, onConsole, onAction, mutating } ) {
386+ function ExtensionCard ( { ext, gpuBackend, agentAvailable, onDetails, onConsole, onAction, mutating, installProgress } ) {
356387 const iconName = ext . features ?. [ 0 ] ?. icon
357388 const Icon = ( iconName && ICON_MAP [ iconName ] ) || Package
358389 const status = ext . status || 'not_installed'
@@ -382,12 +413,16 @@ function ExtensionCard({ ext, gpuBackend, agentAvailable, onDetails, onConsole,
382413 status === 'enabled' ? 'bg-green-500/10' :
383414 status === 'stopped' ? 'bg-red-500/10' :
384415 status === 'incompatible' ? 'bg-orange-500/10' :
416+ ( status === 'installing' || status === 'setting_up' ) ? 'bg-blue-500/10' :
417+ status === 'error' ? 'bg-red-500/10' :
385418 'bg-zinc-800'
386419 } `} >
387420 < Icon size = { 16 } className = {
388421 status === 'enabled' ? 'text-green-400' :
389422 status === 'stopped' ? 'text-red-400' :
390423 status === 'incompatible' ? 'text-orange-400' :
424+ ( status === 'installing' || status === 'setting_up' ) ? 'text-blue-400' :
425+ status === 'error' ? 'text-red-400' :
391426 'text-zinc-400'
392427 } />
393428 </ div >
@@ -406,6 +441,19 @@ function ExtensionCard({ ext, gpuBackend, agentAvailable, onDetails, onConsole,
406441 >
407442 core
408443 </ span >
444+ ) : ( status === 'installing' || status === 'setting_up' ) ? (
445+ < span className = "text-[10px] px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400 flex items-center gap-1" >
446+ < Loader2 size = { 8 } className = "animate-spin" />
447+ { status === 'setting_up' ? 'setting up' : 'installing' }
448+ </ span >
449+ ) : status === 'error' ? (
450+ < span
451+ className = "text-[10px] px-2 py-0.5 rounded-full bg-red-500/20 text-red-300 cursor-pointer"
452+ onClick = { onConsole }
453+ title = "View error details"
454+ >
455+ error
456+ </ span >
409457 ) : (
410458 < span
411459 className = { `text-[10px] px-2 py-0.5 rounded-full uppercase tracking-wider ${ statusStyle } ${ ( status === 'incompatible' || status === 'stopped' ) ? 'cursor-help' : '' } ` }
@@ -437,6 +485,14 @@ function ExtensionCard({ ext, gpuBackend, agentAvailable, onDetails, onConsole,
437485 < p className = "text-xs text-zinc-500 line-clamp-2 leading-relaxed" > { ext . description || 'No description available.' } </ p >
438486 </ div >
439487
488+ { /* Progress indicator */ }
489+ { isMutating && installProgress ?. service_id === ext . id && (
490+ < div className = "px-4 py-2 border-t border-zinc-800/60 text-xs text-blue-300 flex items-center gap-2" >
491+ < Loader2 size = { 12 } className = "animate-spin" />
492+ < span > { installProgress . phase_label || 'Working...' } </ span >
493+ </ div >
494+ ) }
495+
440496 { /* Card footer */ }
441497 < div className = "border-t border-zinc-800/60 px-4 py-2.5 flex items-center justify-between bg-zinc-900/30" >
442498 < div className = "flex gap-1.5" >
0 commit comments