imcp 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,19 +6,27 @@ export function createUninstallCommand(): Command {
6
6
  .description('Uninstall specific MCP servers')
7
7
  .requiredOption(
8
8
  '--category <category>',
9
+ 'Server category'
10
+ )
11
+ .requiredOption(
9
12
  '--names <names>',
10
13
  'Server names (semicolon separated)'
11
14
  )
15
+ .requiredOption(
16
+ '--targets <targets>',
17
+ 'Target clients to uninstall from (semicolon separated)'
18
+ )
12
19
  .option(
13
20
  '--remove-data',
14
21
  'Remove all associated data',
15
- false
22
+ true // Change default to true to ensure cleanup happens
16
23
  )
17
24
  .action(async (options) => {
18
25
  try {
19
26
  const serverNames = options.names.split(';').map((name: string) => name.trim());
20
27
 
21
- if (!serverService.validateServerName(options.category, serverNames)) {
28
+ const validNames = await serverService.validateServerName(options.category, serverNames);
29
+ if (!validNames) {
22
30
  console.error('Invalid server names provided');
23
31
  process.exit(1);
24
32
  }
@@ -26,11 +34,13 @@ export function createUninstallCommand(): Command {
26
34
  console.log(`Uninstalling servers: ${serverNames.join(', ')}`);
27
35
 
28
36
  const results = await Promise.all(
29
- serverNames.map((serverName: string) =>
30
- serverService.uninstallMcpServer(options.category, serverName, {
37
+ serverNames.map((serverName: string) => {
38
+ const targets = options.targets ? options.targets.split(';').map((t: string) => t.trim()) : [];
39
+ return serverService.uninstallMcpServer(options.category, serverName, {
31
40
  removeData: options.removeData,
32
- })
33
- )
41
+ targets: targets
42
+ });
43
+ })
34
44
  );
35
45
 
36
46
  const { success, messages } = serverService.formatOperationResults(results);
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { fileURLToPath } from 'url';
6
- import { GITHUB_REPO, LOCAL_FEEDS_DIR, SETTINGS_DIR } from './constants.js';
6
+ import { GITHUB_REPO, LOCAL_FEEDS_DIR, SETTINGS_DIR, SUPPORTED_CLIENTS } from './constants.js';
7
7
  import { Logger } from '../utils/logger.js';
8
8
  import { checkGithubAuth } from '../utils/githubAuth.js';
9
9
  import {
@@ -13,7 +13,8 @@ import {
13
13
  InstallationStatus,
14
14
  RequirementStatus,
15
15
  MCPServerStatus,
16
- OperationStatus
16
+ OperationStatus,
17
+ ClientSettings
17
18
  } from './types.js';
18
19
  import { ConfigurationLoader } from './ConfigurationLoader.js';
19
20
 
@@ -356,6 +357,74 @@ export class ConfigurationProvider {
356
357
  await this.loadClientMCPSettings();
357
358
  });
358
359
  }
360
+
361
+ async removeServerFromClientMCPSettings(serverName: string, target?: string): Promise<void> {
362
+ return await this.withLock(async () => {
363
+ // Load utils in async context to avoid circular dependencies
364
+ const { readJsonFile, writeJsonFile } = await import('../utils/clientUtils.js');
365
+
366
+ // Filter clients if target is specified
367
+ const clientEntries = Object.entries(SUPPORTED_CLIENTS as Record<string, ClientSettings>);
368
+ const targetClients = target
369
+ ? clientEntries.filter(([clientName]) => clientName === target)
370
+ : clientEntries;
371
+
372
+ for (const [clientName, clientSettings] of targetClients) {
373
+ const settingPath = process.env.CODE_INSIDERS
374
+ ? clientSettings.codeInsiderSettingPath
375
+ : clientSettings.codeSettingPath;
376
+
377
+ try {
378
+ const content = await readJsonFile(settingPath, true);
379
+ let modified = false;
380
+
381
+ // Handle GitHub Copilot's different structure
382
+ if (clientName === 'GithubCopilot' && content.mcp?.servers?.[serverName]) {
383
+ delete content.mcp.servers[serverName];
384
+ modified = true;
385
+ } else if (content.mcpServers?.[serverName]) {
386
+ delete content.mcpServers[serverName];
387
+ modified = true;
388
+ }
389
+
390
+ // Only write if we actually modified the content
391
+ if (modified) {
392
+ await writeJsonFile(settingPath, content);
393
+ Logger.debug(`Removed server ${serverName} from client ${clientName} settings`);
394
+ }
395
+ } catch (error) {
396
+ Logger.error(`Failed to remove server ${serverName} from client ${clientName} settings:`, error);
397
+ }
398
+ }
399
+
400
+ // Also update our in-memory configuration
401
+ if (this.configuration.clientMCPSettings) {
402
+ if (target) {
403
+ // Only update settings for the target client
404
+ const clientSettings = this.configuration.clientMCPSettings[target];
405
+ if (clientSettings) {
406
+ if (clientSettings.mcpServers?.[serverName]) {
407
+ delete clientSettings.mcpServers[serverName];
408
+ }
409
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
410
+ delete clientSettings.servers[serverName];
411
+ }
412
+ }
413
+ } else {
414
+ // Update all clients if no target specified
415
+ for (const clientSettings of Object.values(this.configuration.clientMCPSettings)) {
416
+ if (clientSettings.mcpServers?.[serverName]) {
417
+ delete clientSettings.mcpServers[serverName];
418
+ }
419
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
420
+ delete clientSettings.servers[serverName];
421
+ }
422
+ }
423
+ }
424
+ await this.saveConfiguration();
425
+ }
426
+ });
427
+ }
359
428
  }
360
429
 
361
430
  // Export a singleton instance
@@ -9,6 +9,7 @@ import {
9
9
  ServerCategoryListOptions,
10
10
  ServerOperationResult,
11
11
  ServerUninstallOptions,
12
+ InstallationStatus
12
13
  } from './types.js';
13
14
  import path from 'path';
14
15
 
@@ -93,18 +94,44 @@ export class MCPManager extends EventEmitter {
93
94
  };
94
95
  }
95
96
 
96
- // Clear installation status
97
+ const { targets = [], removeData = false } = options;
98
+
99
+ // Clear installation status for specified targets
100
+ const currentStatus: InstallationStatus = serverCategory.installationStatus || {
101
+ requirementsStatus: {},
102
+ serversStatus: {},
103
+ lastUpdated: new Date().toISOString()
104
+ };
105
+ const serversStatus = currentStatus.serversStatus || {};
106
+ const serverStatus = serversStatus[serverName] || { installedStatus: {}, name: serverName };
107
+
108
+ // Only reset installedStatus for specified targets
109
+ for (const target of targets) {
110
+ if (serverStatus.installedStatus) {
111
+ delete serverStatus.installedStatus[target];
112
+ }
113
+ }
114
+ if (removeData) {
115
+ for (const target of targets) {
116
+ await this.configProvider.removeServerFromClientMCPSettings(serverName, target);
117
+ }
118
+ }
119
+
120
+ // Update server status
121
+ serversStatus[serverName] = serverStatus;
122
+
123
+ // Update status keeping requirements
97
124
  await this.configProvider.updateInstallationStatus(
98
125
  categoryName,
99
- {},
100
- {}
126
+ currentStatus.requirementsStatus || {},
127
+ serversStatus
101
128
  );
102
129
 
103
- this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName });
130
+ this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName, targets });
104
131
 
105
132
  return {
106
133
  success: true,
107
- message: `Successfully uninstalled ${serverName}`,
134
+ message: `Successfully uninstalled ${serverName} from ${targets.join(', ')}`,
108
135
  };
109
136
  } catch (error) {
110
137
  return {
@@ -58,8 +58,18 @@ const CODE_INSIDER_STRORAGE_DIR = (() => {
58
58
  * Value: Client-specific settings or configuration details.
59
59
  * TODO: Define actual client settings structure.
60
60
  */
61
- export const SUPPORTED_CLIENTS: Record<string, any> = {
62
- 'MSRooCode': { /* MSROO specific settings */
61
+ export const SUPPORTED_CLIENTS: Record<string, {
62
+ extension: {
63
+ extensionId: string;
64
+ leastVersion?: string;
65
+ repository?: string;
66
+ assetName?: string;
67
+ private?: boolean;
68
+ };
69
+ codeSettingPath: string;
70
+ codeInsiderSettingPath: string;
71
+ }> = {
72
+ 'MSRooCode': {
63
73
 
64
74
  extension: {
65
75
  extensionId: 'microsoftai.ms-roo-cline',
@@ -140,6 +140,9 @@ export class ExtensionInstaller {
140
140
  } else {
141
141
  // Install private extension from GitHub release using latest version
142
142
  try {
143
+ if (!repository || !assetName) {
144
+ throw new Error(`Missing repository or assetName for private extension ${extensionId}`);
145
+ }
143
146
  const { resolvedPath } = await handleGitHubRelease(
144
147
  { name: extensionId, version: 'latest', type: 'extension' },
145
148
  { repository, assetName }
package/src/core/types.ts CHANGED
@@ -84,6 +84,7 @@ export interface UpdateRequirementOptions {
84
84
 
85
85
  export interface ServerUninstallOptions {
86
86
  removeData?: boolean;
87
+ targets?: string[]; // List of client targets to uninstall from
87
88
  }
88
89
 
89
90
  // Types related to server feed configuration (e.g., ai-coder-tools.json)
@@ -150,6 +151,11 @@ export interface FeedConfiguration {
150
151
  mcpServers: McpConfig[];
151
152
  }
152
153
 
154
+ export interface ClientSettings {
155
+ codeSettingPath: string;
156
+ codeInsiderSettingPath: string;
157
+ }
158
+
153
159
  // Events that can be emitted by the SDK
154
160
  export enum MCPEvent {
155
161
  SERVER_INSTALLED = 'server:installed',
@@ -161,7 +167,7 @@ export enum MCPEvent {
161
167
 
162
168
  export interface MCPEventData {
163
169
  [MCPEvent.SERVER_INSTALLED]: { server: MCPServerCategory };
164
- [MCPEvent.SERVER_UNINSTALLED]: { serverName: string };
170
+ [MCPEvent.SERVER_UNINSTALLED]: { serverName: string; targets?: string[] };
165
171
  [MCPEvent.SERVER_STARTED]: { server: MCPServerCategory };
166
172
  [MCPEvent.SERVER_STOPPED]: { serverName: string };
167
173
  [MCPEvent.CONFIG_CHANGED]: { configuration: MCPConfiguration };
@@ -147,19 +147,34 @@ export class ServerService {
147
147
  * Installs a specific mcp tool for a server.
148
148
  * TODO: This might require enhancing MCPManager to handle category-specific installs.
149
149
  */
150
+ /**
151
+ * Uninstall MCP server from specified client targets
152
+ * @param category The server category
153
+ * @param serverName The server name to uninstall
154
+ * @param options Uninstall options including target clients and data removal flags
155
+ */
150
156
  async uninstallMcpServer(
151
157
  category: string,
152
158
  serverName: string,
153
- options: ServerUninstallOptions = {} // Reuse ServerInstallOptions for env etc.
159
+ options: ServerUninstallOptions = {}
154
160
  ): Promise<ServerOperationResult> {
155
- return mcpManager.uninstallServer(category, serverName, options);
161
+ Logger.debug(`Uninstalling MCP server: ${JSON.stringify({ category, serverName, options })}`);
162
+ try {
163
+ const result = await mcpManager.uninstallServer(category, serverName, options);
164
+ Logger.debug(`Uninstallation result: ${JSON.stringify(result)}`);
165
+ return result;
166
+ } catch (error) {
167
+ Logger.error(`Failed to uninstall MCP server: ${serverName}`, error);
168
+ throw error;
169
+ }
156
170
  }
157
171
 
158
172
  /**
159
173
  * Validates server names
160
174
  */
161
- async validateServerName(category: string, name: string): Promise<boolean> {
162
- Logger.debug(`Validating server name: ${JSON.stringify({ category, name })}`);
175
+ async validateServerName(category: string, names: string | string[]): Promise<boolean> {
176
+ const serverNames = Array.isArray(names) ? names : [names];
177
+ Logger.debug(`Validating server names: ${JSON.stringify({ category, names: serverNames })}`);
163
178
 
164
179
  // Check if category exists in feeds
165
180
  const feedConfig = await mcpManager.getFeedConfiguration(category);
@@ -168,10 +183,13 @@ export class ServerService {
168
183
  return false;
169
184
  }
170
185
 
171
- // Check if server exists in the category's mcpServers
172
- const serverExists = feedConfig.mcpServers.some(server => server.name === name);
173
- if (!serverExists) {
174
- Logger.debug(`Validation failed: Server "${name}" not found in category "${category}"`);
186
+ // Check if all servers exist in the category's mcpServers
187
+ const invalidServers = serverNames.filter(name =>
188
+ !feedConfig.mcpServers.some(server => server.name === name)
189
+ );
190
+
191
+ if (invalidServers.length > 0) {
192
+ Logger.debug(`Validation failed: Servers "${invalidServers.join(', ')}" not found in category "${category}"`);
175
193
  return false;
176
194
  }
177
195
 
@@ -273,7 +273,7 @@ export function getPythonPackagePath(pythonExecutablePath: string): string {
273
273
  // Virtual environment structure on Windows: <venv>/Scripts/python.exe
274
274
  const venvRoot = path.dirname(dir);
275
275
  return path.join(venvRoot, 'Lib', 'site-packages');
276
- } else if (dir.toLowerCase().includes('python')) {
276
+ } else {
277
277
  // System Python or Conda on Windows
278
278
  return path.join(dir, 'Lib', 'site-packages');
279
279
  }
@@ -160,6 +160,15 @@ body {
160
160
  transition: all 0.2s ease;
161
161
  cursor: pointer;
162
162
  user-select: none;
163
+ gap: 0.5rem;
164
+ }
165
+
166
+ /* Client actions container */
167
+ .client-actions {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.5rem;
171
+ margin-left: auto;
163
172
  }
164
173
 
165
174
  .client-item:hover {
@@ -180,6 +189,14 @@ body {
180
189
  gap: 0.75rem;
181
190
  }
182
191
 
192
+ /* Status container */
193
+ .status-container {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 0.5rem;
197
+ margin-left: auto;
198
+ }
199
+
183
200
  /* Status badges */
184
201
  .status-badge {
185
202
  display: inline-flex;
@@ -227,6 +244,31 @@ body {
227
244
  background-color: #fef3c7;
228
245
  }
229
246
 
247
+ /* Uninstall button styling */
248
+ .uninstall-btn {
249
+ display: inline-flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ width: 28px;
253
+ height: 28px;
254
+ border-radius: 6px;
255
+ border: 1px solid transparent;
256
+ background: transparent;
257
+ cursor: pointer;
258
+ transition: all 0.2s ease;
259
+ padding: 0;
260
+ }
261
+
262
+ .uninstall-btn:hover {
263
+ background-color: #fee2e2;
264
+ border-color: #ef4444;
265
+ transform: scale(1.05);
266
+ }
267
+
268
+ .uninstall-btn i {
269
+ font-size: 1.25rem;
270
+ }
271
+
230
272
  /* Environment variables section */
231
273
  #modalEnvInputs input {
232
274
  width: 100%;
@@ -141,14 +141,16 @@ function _appendInstallLoadingMessage(message) {
141
141
  /**
142
142
  * Display the installation loading modal and prepare it for messages.
143
143
  */
144
- function showInstallLoadingModal() {
144
+ function showInstallLoadingModal(operation = 'Installing') {
145
145
  const loadingModal = document.getElementById('installLoadingModal');
146
146
  const loadingMsg = document.getElementById('installLoadingMessage');
147
- if (loadingModal && loadingMsg) {
147
+ const loadingTitle = document.querySelector('.loading-title');
148
+ if (loadingModal && loadingMsg && loadingTitle) {
148
149
  loadingModal.style.display = 'block';
149
150
  loadingMsg.innerHTML = '';
151
+ loadingTitle.textContent = `${operation}...`;
150
152
  } else {
151
- console.error('[LoadingModal] Required elements not found: installLoadingModal or installLoadingMessage');
153
+ console.error('[LoadingModal] Required elements not found: installLoadingModal, installLoadingMessage, or loading-title');
152
154
  }
153
155
  }
154
156
 
@@ -330,17 +332,55 @@ async function showInstallModal(categoryName, serverName, callback) {
330
332
  // Add elements to client info
331
333
  clientInfo.appendChild(clientName);
332
334
 
333
- // Add client info (name) to the item first
335
+ // Add elements to client item
334
336
  clientItem.appendChild(clientInfo);
335
337
 
338
+ // Status container for badge and uninstall button
339
+ const statusContainer = document.createElement('div');
340
+ statusContainer.className = 'status-container';
341
+
336
342
  // Status badge - only show if we have status text
337
343
  if (statusText) {
338
344
  const statusBadge = document.createElement('span');
339
345
  statusBadge.className = `status-badge ${statusClass}`;
340
346
  statusBadge.textContent = statusText;
341
- clientItem.appendChild(statusBadge);
347
+ statusContainer.appendChild(statusBadge);
348
+
349
+ // Add uninstall button right after status badge if installed
350
+ if (operationStatus.status === 'completed' && operationStatus.type === 'install') {
351
+ const uninstallBtn = document.createElement('button');
352
+ uninstallBtn.className = 'uninstall-btn text-red-600 hover:text-red-800 ml-2';
353
+ uninstallBtn.innerHTML = '<i class="bx bx-trash"></i>';
354
+ uninstallBtn.title = 'Uninstall from this client';
355
+ uninstallBtn.onclick = async (e) => {
356
+ e.stopPropagation(); // Prevent item selection
357
+ e.preventDefault(); // Prevent form submission
358
+ const confirmed = await showConfirm('Uninstall Confirmation', `Are you sure you want to uninstall ${serverName} from ${target}?`);
359
+ if (confirmed) {
360
+ // Add target to selectedClients for uninstallation
361
+ window.selectedClients = [target];
362
+ showInstallLoadingModal('Uninstalling');
363
+ const serverList = {
364
+ [serverName]: {
365
+ removeData: true // Include removal of associated data
366
+ }
367
+ };
368
+ try {
369
+ delayedAppendInstallLoadingMessage(`Uninstalling ${serverName} from ${target}...`);
370
+ await uninstallTools(categoryName, serverList, [target]);
371
+ } catch (error) {
372
+ delayedAppendInstallLoadingMessage(`Error: ${error.message}`);
373
+ throw error; // Re-throw to trigger error handling in uninstallTools
374
+ }
375
+ }
376
+ return false; // Prevent form submission
377
+ };
378
+ statusContainer.appendChild(uninstallBtn);
379
+ }
342
380
  }
343
381
 
382
+ clientItem.appendChild(statusContainer);
383
+
344
384
  // Add client item to target div
345
385
  targetDiv.appendChild(clientItem);
346
386
  });
@@ -641,34 +681,40 @@ async function showInstallModal(categoryName, serverName, callback) {
641
681
  });
642
682
  });
643
683
 
644
- // Get selected clients
645
- const selectedTargets = window.selectedClients.length > 0 ?
646
- window.selectedClients :
647
- Array.from(document.querySelectorAll('.client-item.selected'))
648
- .map(item => item.dataset.target);
649
-
650
684
  // Check if we have any requirements selected for update
651
685
  const hasRequirementsToUpdate = requirementsToUpdate.length > 0;
652
686
 
653
- // Only require client selection if we don't have any requirements to update
654
- if (selectedTargets.length === 0 && !hasRequirementsToUpdate) {
655
- showToast('Please select at least one client to configure.', 'error');
656
- return;
687
+ // Only proceed if this isn't an uninstall operation
688
+ const uninstallBtn = document.querySelector('.uninstall-btn');
689
+ if (!uninstallBtn || !uninstallBtn.matches(':active')) {
690
+ // Get selected clients
691
+ const selectedTargets = window.selectedClients.length > 0 ?
692
+ window.selectedClients :
693
+ Array.from(document.querySelectorAll('.client-item.selected'))
694
+ .map(item => item.dataset.target);
695
+
696
+ console.log('Selected targets:', selectedTargets);
697
+ console.log('Requirements to update:', requirementsToUpdate);
698
+ if (selectedTargets.length === 0 && !hasRequirementsToUpdate) {
699
+ showToast('Please select at least one client to configure.', 'error');
700
+ return;
701
+ }
702
+ window.selectedClients = selectedTargets;
657
703
  }
658
704
 
659
705
  // Call install function with selected targets
660
706
  // Find installing message for the first selected target
661
707
  let installingMessage = "Starting installation...";
662
708
  const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
663
- if (selectedTargets.length > 0) {
664
- const target = selectedTargets[0];
709
+ if (window.selectedClients.length > 0) {
710
+ const target = window.selectedClients[0];
665
711
  const msg = serverStatus.installedStatus?.[target]?.message;
666
712
  if (msg) installingMessage = msg;
667
713
  }
668
714
 
669
715
  // Add requirements to update to serverInstallOptions if any
670
716
  const serverInstallOptions = {
671
- targetClients: selectedTargets,
717
+ targetClients: window.selectedClients,
672
718
  env: envVars,
673
719
  args: args,
674
720
  settings: pythonEnv ? { pythonEnv } : undefined
@@ -679,7 +725,9 @@ async function showInstallModal(categoryName, serverName, callback) {
679
725
  serverInstallOptions.requirements = requirementsToUpdate;
680
726
  }
681
727
 
682
- handleBulkClientInstall(categoryName, serverName, selectedTargets, envVars, installingMessage, serverData, serverInstallOptions);
728
+ // For installation, use the selectedTargets from the validation above
729
+ const targetsToUse = document.querySelector('.uninstall-btn:hover') ? [] : window.selectedClients;
730
+ handleBulkClientInstall(categoryName, serverName, targetsToUse, envVars, installingMessage, serverData, serverInstallOptions);
683
731
  };
684
732
 
685
733
  } catch (error) {
@@ -893,23 +941,35 @@ async function pollInstallStatus(categoryName, serverName, targets, interval = 2
893
941
 
894
942
  // Function to handle client uninstallation for multiple targets
895
943
  async function uninstallTools(categoryName, serverList, targets) {
896
- if (!Array.isArray(targets)) {
897
- targets = [targets]; // Convert single target to array for backward compatibility
898
- }
899
-
900
- const confirmed = await showConfirm(`Are you sure you want to uninstall this server for ${targets.length} client(s)?`);
901
- if (!confirmed) {
944
+ // Get selected targets from window.selectedClients or the provided targets
945
+ const selectedTargets = window.selectedClients || (Array.isArray(targets) ? targets : [targets]);
946
+
947
+ // Validate selected targets
948
+ if (!selectedTargets || selectedTargets.length === 0) {
949
+ showToast('Please select at least one client to uninstall.', 'error');
902
950
  return;
903
951
  }
904
952
 
905
953
  try {
954
+ delayedAppendInstallLoadingMessage('Starting uninstallation...');
955
+
956
+ // Ensure serverList is an object where each key is a server name
957
+ if (Array.isArray(serverList)) {
958
+ const formattedServerList = {};
959
+ serverList.forEach(server => {
960
+ formattedServerList[server] = { removeData: true };
961
+ });
962
+ serverList = formattedServerList;
963
+ }
964
+
906
965
  const response = await fetch(`/api/categories/${categoryName}/uninstall`, {
907
966
  method: 'POST',
908
967
  headers: { 'Content-Type': 'application/json' },
909
968
  body: JSON.stringify({
910
- toolList: serverList,
969
+ serverList: serverList,
911
970
  options: {
912
- targets: targets
971
+ targets: selectedTargets,
972
+ removeData: true
913
973
  }
914
974
  })
915
975
  });
@@ -924,10 +984,14 @@ async function uninstallTools(categoryName, serverList, targets) {
924
984
  throw new Error(result.error || 'Uninstallation failed');
925
985
  }
926
986
 
927
- showToast(`Successfully uninstalled for ${targets.length} client(s).`, 'success');
928
- location.reload(); // Refresh the page to update the UI
987
+ // Add completion message and trigger completion UI
988
+ delayedAppendInstallLoadingMessage(`Successfully uninstalled from ${selectedTargets.join(', ')}`, true);
989
+
990
+ // Clear selected clients after successful uninstall
991
+ window.selectedClients = [];
929
992
  } catch (error) {
930
993
  console.error('Error uninstalling tools:', error);
994
+ delayedAppendInstallLoadingMessage(`Error: ${error.message}`, true);
931
995
  showToast(`Error uninstalling tools: ${error.message}`, 'error');
932
996
  }
933
997
  }