@@ -22,6 +22,7 @@ import {
2222} from 'lucide-react' ;
2323import { ServerActionMenu } from './ServerActionMenu' ;
2424import { CloneAccountModal } from './CloneAccountModal' ;
25+ import { UninstallSourceWithClonesDialog } from './UninstallSourceWithClonesDialog' ;
2526import type { ServerViewModel , ServerDefinition , InstalledServerState , InputDefinition } from '../../types/registry' ;
2627import type { ServerFeature } from '@/lib/api/serverFeatures' ;
2728import { listServerFeaturesByServer } from '@/lib/api/serverFeatures' ;
@@ -38,6 +39,7 @@ import { ConfigEditorModal } from '@/components/ConfigEditorModal';
3839import { ServerDefinitionModal } from '@/components/ServerDefinitionModal' ;
3940import { SourceBadge } from '@/components/SourceBadge' ;
4041import type { ClonedInstalledServer } from '@/lib/api/serverClone' ;
42+ import { listCloneDependents } from '@/lib/api/serverClone' ;
4143
4244/** Server view model extended with optional clone lineage from the backend. */
4345type ServerViewModelWithClone = ServerViewModel & { cloned_from ?: string } ;
@@ -220,6 +222,12 @@ export function ServersPage() {
220222
221223 // Clone account wizard state
222224 const [ cloneModalServer , setCloneModalServer ] = useState < ServerViewModelWithClone | null > ( null ) ;
225+
226+ // Uninstall source-with-clones confirmation
227+ const [ uninstallClonesDialog , setUninstallClonesDialog ] = useState < {
228+ server : ServerViewModelWithClone ;
229+ dependents : ClonedInstalledServer [ ] ;
230+ } | null > ( null ) ;
223231
224232 // Config editor state
225233 const [ editConfigSpace , setEditConfigSpace ] = useState < { id : string ; name : string } | null > ( null ) ;
@@ -795,31 +803,103 @@ export function ServersPage() {
795803 }
796804 } ;
797805
798- const handleUninstall = async ( server : ServerViewModel ) => {
806+ const performUninstall = async ( serverIds : string [ ] ) => {
807+ const { uninstallServer } = await import ( '@/lib/api/registry' ) ;
808+ const { disconnectServer } = await import ( '@/lib/api/gateway' ) ;
809+
810+ if ( gatewayRunning && viewSpace ) {
811+ for ( const serverId of serverIds ) {
812+ const target = installedServers . find ( ( entry ) => entry . id === serverId ) ;
813+ if ( ! target ?. enabled ) {
814+ continue ;
815+ }
816+
817+ try {
818+ await disconnectServer ( serverId , viewSpace . id ) ;
819+ } catch ( error ) {
820+ console . warn ( `[ServersPage] Failed to disconnect server from gateway:` , error ) ;
821+ }
822+ }
823+ }
824+
825+ for ( const serverId of serverIds ) {
826+ await uninstallServer ( serverId , viewSpace ?. id ?? '' ) ;
827+ }
828+
829+ await loadData ( ) ;
830+ } ;
831+
832+ const handleUninstall = async ( server : ServerViewModelWithClone ) => {
833+ if ( ! viewSpace ) {
834+ return ;
835+ }
836+
837+ if ( ! server . cloned_from ) {
838+ try {
839+ const dependents = await listCloneDependents ( viewSpace . id , server . id ) ;
840+ if ( dependents . length > 0 ) {
841+ setUninstallClonesDialog ( { server, dependents } ) ;
842+ return ;
843+ }
844+ } catch ( error ) {
845+ showToast ( String ( error ) , 'error' ) ;
846+ return ;
847+ }
848+ }
849+
799850 const { getUninstallLabel } = await import ( '@/components/SourceBadge' ) ;
800851 const actionLabel = getUninstallLabel ( server . installation_source ) ;
801852
802853 setActionLoading ( `uninstall-${ server . id } ` ) ;
803854 try {
804- const { uninstallServer } = await import ( '@/lib/api/registry' ) ;
805- const { disconnectServer } = await import ( '@/lib/api/gateway' ) ;
806-
807- if ( gatewayRunning && server . enabled && viewSpace ) {
808- try {
809- await disconnectServer ( server . id , viewSpace . id ) ;
810- } catch ( e ) {
811- console . warn ( `[ServersPage] Failed to disconnect server from gateway:` , e ) ;
812- }
813- }
814-
815- // ServerAppService handles source-aware cleanup automatically:
816- // - UserConfig: removes from JSON file + DB
817- // - Registry/ManualEntry: just removes from DB
818- await uninstallServer ( server . id , viewSpace ?. id ?? '' ) ;
819- await loadData ( ) ;
855+ await performUninstall ( [ server . id ] ) ;
820856 showToast ( `${ server . name } ${ actionLabel . toLowerCase ( ) } ed` , 'success' ) ;
821- } catch ( e ) {
822- showToast ( String ( e ) , 'error' ) ;
857+ } catch ( error ) {
858+ showToast ( String ( error ) , 'error' ) ;
859+ } finally {
860+ setActionLoading ( null ) ;
861+ }
862+ } ;
863+
864+ const handleUninstallSourceOnly = async ( ) => {
865+ if ( ! uninstallClonesDialog ) {
866+ return ;
867+ }
868+
869+ const { server } = uninstallClonesDialog ;
870+ const { getUninstallLabel } = await import ( '@/components/SourceBadge' ) ;
871+ const actionLabel = getUninstallLabel ( server . installation_source ) ;
872+
873+ setUninstallClonesDialog ( null ) ;
874+ setActionLoading ( `uninstall-${ server . id } ` ) ;
875+ try {
876+ await performUninstall ( [ server . id ] ) ;
877+ showToast ( `${ server . name } ${ actionLabel . toLowerCase ( ) } ed` , 'success' ) ;
878+ } catch ( error ) {
879+ showToast ( String ( error ) , 'error' ) ;
880+ } finally {
881+ setActionLoading ( null ) ;
882+ }
883+ } ;
884+
885+ const handleUninstallAllWithClones = async ( ) => {
886+ if ( ! uninstallClonesDialog ) {
887+ return ;
888+ }
889+
890+ const { server, dependents } = uninstallClonesDialog ;
891+ const serverIds = [ ...dependents . map ( ( dependent ) => dependent . server_id ) , server . id ] ;
892+
893+ setUninstallClonesDialog ( null ) ;
894+ setActionLoading ( `uninstall-${ server . id } ` ) ;
895+ try {
896+ await performUninstall ( serverIds ) ;
897+ showToast (
898+ `${ server . name } and ${ dependents . length } clone${ dependents . length === 1 ? '' : 's' } uninstalled` ,
899+ 'success'
900+ ) ;
901+ } catch ( error ) {
902+ showToast ( String ( error ) , 'error' ) ;
823903 } finally {
824904 setActionLoading ( null ) ;
825905 }
@@ -930,6 +1010,16 @@ export function ServersPage() {
9301010 return (
9311011 < div className = "space-y-6" data-testid = "servers-page" >
9321012 { gatewayControl . ConfirmDialogElement }
1013+ { uninstallClonesDialog && (
1014+ < UninstallSourceWithClonesDialog
1015+ open
1016+ sourceName = { uninstallClonesDialog . server . name }
1017+ dependents = { uninstallClonesDialog . dependents }
1018+ onCancel = { ( ) => setUninstallClonesDialog ( null ) }
1019+ onUninstallSourceOnly = { handleUninstallSourceOnly }
1020+ onUninstallAll = { handleUninstallAllWithClones }
1021+ />
1022+ ) }
9331023 { /* Header */ }
9341024 < div className = "flex items-center justify-between" >
9351025 < div >
0 commit comments