@@ -272,8 +272,11 @@ function DevToolsContent({ world }) {
272272 const [ allApps , setAllApps ] = useState ( [ ] )
273273 const [ connectionStatus , setConnectionStatus ] = useState ( 'disconnected' )
274274 const [ isConnecting , setIsConnecting ] = useState ( false )
275+ const [ isLoadingApps , setIsLoadingApps ] = useState ( false )
275276 const [ lastError , setLastError ] = useState ( null )
277+ const [ lastSuccess , setLastSuccess ] = useState ( null )
276278 const [ activeTab , setActiveTab ] = useState ( 'linked' )
279+ const [ actionLoading , setActionLoading ] = useState ( { } )
277280
278281 useEffect ( ( ) => {
279282 // Initialize server URL from AppServerClient if available
@@ -288,9 +291,11 @@ function DevToolsContent({ world }) {
288291 // Listen for app linked/unlinked events
289292 const onAppLinked = ( { appName, linkInfo } ) => {
290293 loadApps ( )
294+ showSuccess ( `${ appName } linked successfully` )
291295 }
292296 const onAppUnlinked = ( { appName } ) => {
293297 loadApps ( )
298+ showSuccess ( `${ appName } unlinked successfully` )
294299 }
295300
296301 world . on ( 'app_linked' , onAppLinked )
@@ -302,29 +307,56 @@ function DevToolsContent({ world }) {
302307 }
303308 } , [ ] )
304309
310+ // Utility functions for showing feedback
311+ const showSuccess = ( message ) => {
312+ setLastSuccess ( message )
313+ setLastError ( null )
314+ setTimeout ( ( ) => setLastSuccess ( null ) , 3000 )
315+ }
316+
317+ const showError = ( message ) => {
318+ setLastError ( message )
319+ setLastSuccess ( null )
320+ }
321+
322+ const setActionLoadingState = ( action , isLoading ) => {
323+ setActionLoading ( prev => ( { ...prev , [ action ] : isLoading } ) )
324+ }
325+
305326 const checkConnection = async ( ) => {
306327 try {
307328 setIsConnecting ( true )
308329 setLastError ( null )
330+ setLastSuccess ( null )
309331
310332 const response = await fetch ( `${ serverUrl } /health` )
311333 if ( response . ok ) {
312334 setConnectionStatus ( 'connected' )
335+ showSuccess ( 'Connected to development server' )
313336 await loadApps ( )
314337 } else {
315338 setConnectionStatus ( 'error' )
316- setLastError ( 'Server responded with error' )
339+ showError ( 'Server responded with error' )
317340 }
318341 } catch ( error ) {
319342 setConnectionStatus ( 'disconnected' )
320- setLastError ( error . message )
343+ showError ( `Connection failed: ${ error . message } ` )
321344 } finally {
322345 setIsConnecting ( false )
323346 }
324347 }
325348
349+ const disconnect = ( ) => {
350+ setConnectionStatus ( 'disconnected' )
351+ setLinkedApps ( [ ] )
352+ setAllApps ( [ ] )
353+ showSuccess ( 'Disconnected from development server' )
354+ }
355+
326356 const loadApps = async ( ) => {
327357 try {
358+ setIsLoadingApps ( true )
359+
328360 // Load all apps
329361 const allAppsResponse = await fetch ( `${ serverUrl } /api/apps` )
330362 if ( allAppsResponse . ok ) {
@@ -333,14 +365,17 @@ function DevToolsContent({ world }) {
333365 }
334366
335367 // Load linked apps for current world
336- const worldUrl = world . network ?. ws ?. url || 'unknown'
368+ const worldUrl = world . network ?. apiUrl . split ( "/api" ) [ 0 ]
337369 const linkedAppsResponse = await fetch ( `${ serverUrl } /api/linked-apps?worldUrl=${ encodeURIComponent ( worldUrl ) } ` )
338370 if ( linkedAppsResponse . ok ) {
339371 const { apps } = await linkedAppsResponse . json ( )
340372 setLinkedApps ( apps || [ ] )
341373 }
342374 } catch ( error ) {
343375 console . warn ( 'Failed to load apps:' , error )
376+ showError ( 'Failed to load apps list' )
377+ } finally {
378+ setIsLoadingApps ( false )
344379 }
345380 }
346381
@@ -356,53 +391,48 @@ function DevToolsContent({ world }) {
356391
357392 const unlinkApp = async ( appName ) => {
358393 try {
394+ setActionLoadingState ( `unlink-${ appName } ` , true )
395+
359396 const response = await fetch ( `${ serverUrl } /api/apps/${ appName } /unlink` , {
360397 method : 'POST'
361398 } )
362399
363400 if ( response . ok ) {
364- console . log ( `✅ Unlinked ${ appName } `)
401+ showSuccess ( ` ${ appName } unlinked successfully `)
365402 await loadApps ( )
366403 } else {
367404 throw new Error ( 'Failed to unlink app' )
368405 }
369406 } catch ( error ) {
370407 console . error ( `❌ Failed to unlink ${ appName } :` , error )
371- alert ( `Failed to unlink ${ appName } : ${ error . message } ` )
408+ showError ( `Failed to unlink ${ appName } : ${ error . message } ` )
409+ } finally {
410+ setActionLoadingState ( `unlink-${ appName } ` , false )
372411 }
373412 }
374413
375414 const pushApp = async ( appName ) => {
376415 try {
377- // Find the app in the current world
378- let targetApp = null
379- for ( const [ _ , entity ] of world . entities . items ) {
380- if ( entity . isApp && entity . blueprint ?. name === appName ) {
381- targetApp = entity
382- break
383- }
384- }
385-
386- if ( ! targetApp ) {
387- throw new Error ( `App ${ appName } not found in current world` )
388- }
416+ setActionLoadingState ( `push-${ appName } ` , true )
389417
390418 const response = await fetch ( `${ serverUrl } /api/apps/${ appName } /deploy` , {
391419 method : 'POST' ,
392420 headers : { 'Content-Type' : 'application/json' } ,
393421 body : JSON . stringify ( {
394- position : targetApp . data . position || [ 0 , 0 , 0 ]
422+ position : [ 0 , 0 , 0 ]
395423 } )
396424 } )
397425
398426 if ( response . ok ) {
399- console . log ( `🚀 Deployed ${ appName } from development server `)
427+ showSuccess ( ` ${ appName } deployed successfully `)
400428 } else {
401429 throw new Error ( 'Failed to deploy app' )
402430 }
403431 } catch ( error ) {
404432 console . error ( `❌ Failed to deploy ${ appName } :` , error )
405- alert ( `Failed to deploy ${ appName } : ${ error . message } ` )
433+ showError ( `Failed to deploy ${ appName } : ${ error . message } ` )
434+ } finally {
435+ setActionLoadingState ( `push-${ appName } ` , false )
406436 }
407437 }
408438
@@ -417,33 +447,93 @@ function DevToolsContent({ world }) {
417447
418448 < FieldText
419449 label = 'Server Port'
420- hint = ' Port number for the local development server'
450+ hint = { connectionStatus === 'connected' ? 'Disconnect to change port' : ' Port number for the local development server'}
421451 value = { customPort }
422- onChange = { setCustomPort }
452+ onChange = { connectionStatus === 'connected' ? ( ) => { } : setCustomPort }
423453 />
424454
425- < FieldBtn
426- label = 'Connect'
427- hint = 'Connect to the development server'
428- onClick = { connectWithCustomPort }
429- disabled = { isConnecting }
430- />
455+ { connectionStatus !== 'connected' && (
456+ < FieldBtn
457+ label = { isConnecting ? 'Connecting...' : 'Connect' }
458+ hint = 'Connect to the development server'
459+ onClick = { connectWithCustomPort }
460+ disabled = { isConnecting }
461+ />
462+ ) }
463+
464+ { connectionStatus === 'connected' && (
465+ < FieldBtn
466+ label = 'Disconnect'
467+ hint = 'Disconnect from the development server'
468+ onClick = { disconnect }
469+ disabled = { isConnecting }
470+ />
471+ ) }
431472
432473 < FieldBtn
433- label = ' Refresh Status'
474+ label = { isConnecting ? 'Checking...' : ' Refresh Status'}
434475 hint = 'Check the current connection status'
435476 onClick = { checkConnection }
436477 disabled = { isConnecting }
437478 />
438479
439- { connectionStatus === 'connected' && (
440- < FieldBtn
441- label = 'Open Server UI'
442- hint = 'Open the development server web interface'
443- onClick = { ( ) => window . open ( serverUrl , '_blank' ) }
444- />
480+ { /* Connection Status Display */ }
481+ < div
482+ css = { css `
483+ margin : 0.5rem 1rem ;
484+ padding : 0.75rem 1rem ;
485+ border-radius : 0.375rem ;
486+ font-size : 0.9rem ;
487+ display : flex;
488+ align-items : center;
489+ gap : 0.5rem ;
490+ ${ connectionStatus === 'connected' && `
491+ background: rgba(16, 185, 129, 0.1);
492+ border: 1px solid rgba(16, 185, 129, 0.3);
493+ color: #a7f3d0;
494+ ` }
495+ ${ connectionStatus === 'disconnected' && `
496+ background: rgba(156, 163, 175, 0.1);
497+ border: 1px solid rgba(156, 163, 175, 0.3);
498+ color: #d1d5db;
499+ ` }
500+ ${ connectionStatus === 'error' && `
501+ background: rgba(239, 68, 68, 0.1);
502+ border: 1px solid rgba(239, 68, 68, 0.3);
503+ color: #fca5a5;
504+ ` }
505+ ` }
506+ >
507+ { connectionStatus === 'connected' && < CheckCircleIcon size = { 16 } /> }
508+ { connectionStatus === 'disconnected' && < WifiOffIcon size = { 16 } /> }
509+ { connectionStatus === 'error' && < XCircleIcon size = { 16 } /> }
510+ { connectionStatus === 'connected' && `Connected to ${ serverUrl } ` }
511+ { connectionStatus === 'disconnected' && 'Not connected to development server' }
512+ { connectionStatus === 'error' && 'Connection error' }
513+ </ div >
514+
515+ { /* Success Message */ }
516+ { lastSuccess && (
517+ < div
518+ css = { css `
519+ background : rgba (16 , 185 , 129 , 0.1 );
520+ border : 1px solid rgba (16 , 185 , 129 , 0.3 );
521+ border-radius : 0.375rem ;
522+ padding : 0.75rem 1rem ;
523+ color : # a7f3d0 ;
524+ font-size : 0.9rem ;
525+ margin : 0.5rem 1rem ;
526+ display : flex;
527+ align-items : center;
528+ gap : 0.5rem ;
529+ ` }
530+ >
531+ < CheckCircleIcon size = { 16 } />
532+ { lastSuccess }
533+ </ div >
445534 ) }
446535
536+ { /* Error Message */ }
447537 { lastError && (
448538 < div
449539 css = { css `
@@ -454,8 +544,12 @@ function DevToolsContent({ world }) {
454544 color : # fca5a5 ;
455545 font-size : 0.9rem ;
456546 margin : 0.5rem 1rem ;
547+ display : flex;
548+ align-items : center;
549+ gap : 0.5rem ;
457550 ` }
458551 >
552+ < XCircleIcon size = { 16 } />
459553 { lastError }
460554 </ div >
461555 ) }
@@ -536,43 +630,36 @@ function DevToolsContent({ world }) {
536630 < div className = "app-item-info-name" > { app . name } </ div >
537631 < div className = "app-item-info-details" >
538632 { app . assets . length } assets • { app . script ? 'Has script' : 'No script' }
539- { app . linkInfo && < > • Linked to { ( ( ) => {
540- try {
541- return new URL ( app . linkInfo . worldUrl ) . pathname
542- } catch {
543- return app . linkInfo . worldUrl
544- }
545- } ) ( ) } </ > }
546633 </ div >
547634 </ div >
548635 < div className = "app-item-actions" >
549636 < div
550- className = " app-item-btn"
551- onClick = { ( ) => pushApp ( app . name ) }
637+ className = { ` app-item-btn ${ actionLoading [ `push- ${ app . name } ` ] ? 'loading' : '' } ` }
638+ onClick = { ( ) => ! actionLoading [ `push- ${ app . name } ` ] && pushApp ( app . name ) }
552639 title = "Deploy local changes to world"
640+ style = { { opacity : actionLoading [ `push-${ app . name } ` ] ? 0.5 : 1 } }
553641 >
554- < UploadIcon size = { 14 } />
642+ { actionLoading [ `push-${ app . name } ` ] ? (
643+ < RefreshCwIcon size = { 14 } className = "spin" />
644+ ) : (
645+ < UploadIcon size = { 14 } />
646+ ) }
555647 </ div >
556648 < div
557- className = " app-item-btn"
649+ className = { ` app-item-btn danger ${ actionLoading [ `unlink- ${ app . name } ` ] ? 'loading' : '' } ` }
558650 onClick = { ( ) => {
559- console . log ( `App folder: tools/apps/${ app . name } /` )
560- alert ( `App folder: tools/apps/${ app . name } /\n\nEdit index.js to modify the app.` )
561- } }
562- title = "Show app folder location"
563- >
564- < ExternalLinkIcon size = { 14 } />
565- </ div >
566- < div
567- className = "app-item-btn danger"
568- onClick = { ( ) => {
569- if ( confirm ( `Unlink ${ app . name } ? This will remove the connection but keep the local files.` ) ) {
651+ if ( ! actionLoading [ `unlink-${ app . name } ` ] && confirm ( `Unlink ${ app . name } ? This will remove the connection but keep the local files.` ) ) {
570652 unlinkApp ( app . name )
571653 }
572654 } }
573655 title = "Unlink app from development server"
656+ style = { { opacity : actionLoading [ `unlink-${ app . name } ` ] ? 0.5 : 1 } }
574657 >
575- < TrashIcon size = { 14 } />
658+ { actionLoading [ `unlink-${ app . name } ` ] ? (
659+ < RefreshCwIcon size = { 14 } className = "spin" />
660+ ) : (
661+ < TrashIcon size = { 14 } />
662+ ) }
576663 </ div >
577664 </ div >
578665 </ div >
@@ -601,21 +688,16 @@ function DevToolsContent({ world }) {
601688 </ div >
602689 < div className = "app-item-actions" >
603690 < div
604- className = " app-item-btn"
605- onClick = { ( ) => pushApp ( app . name ) }
691+ className = { ` app-item-btn ${ actionLoading [ `push- ${ app . name } ` ] ? 'loading' : '' } ` }
692+ onClick = { ( ) => ! actionLoading [ `push- ${ app . name } ` ] && pushApp ( app . name ) }
606693 title = "Deploy app to world"
694+ style = { { opacity : actionLoading [ `push-${ app . name } ` ] ? 0.5 : 1 } }
607695 >
608- < UploadIcon size = { 14 } />
609- </ div >
610- < div
611- className = "app-item-btn"
612- onClick = { ( ) => {
613- console . log ( `App folder: tools/apps/${ app . name } /` )
614- alert ( `App folder: tools/apps/${ app . name } /\n\nEdit index.js to modify the app.` )
615- } }
616- title = "Show app folder location"
617- >
618- < ExternalLinkIcon size = { 14 } />
696+ { actionLoading [ `push-${ app . name } ` ] ? (
697+ < RefreshCwIcon size = { 14 } className = "spin" />
698+ ) : (
699+ < UploadIcon size = { 14 } />
700+ ) }
619701 </ div >
620702 </ div >
621703 </ div >
@@ -628,10 +710,10 @@ function DevToolsContent({ world }) {
628710 < Group label = 'Quick Actions' />
629711
630712 < FieldBtn
631- label = ' Refresh Apps'
713+ label = { isLoadingApps ? 'Refreshing...' : ' Refresh Apps'}
632714 hint = 'Manually refresh the list of apps'
633715 onClick = { loadApps }
634- disabled = { connectionStatus !== 'connected' }
716+ disabled = { connectionStatus !== 'connected' || isLoadingApps }
635717 />
636718 </ div >
637719 )
0 commit comments