Skip to content

feat: add component uninstall support#77

Open
Raven95676 wants to merge 1 commit into
mainfrom
feat/component-uninstall
Open

feat: add component uninstall support#77
Raven95676 wants to merge 1 commit into
mainfrom
feat/component-uninstall

Conversation

@Raven95676

@Raven95676 Raven95676 commented Jul 3, 2026

Copy link
Copy Markdown
Member

Add trash icon uninstall button in component management card, with confirmation modal. Backend removes the component directory for Python runtimes, Node.js, and uv.

Summary by Sourcery

Add support for uninstalling runtime components from the UI and backend.

New Features:

  • Add uninstall button with confirmation modal for components in the Versions page.
  • Expose a new uninstall_component Tauri command and wire it through the frontend API and operation keys.

Enhancements:

  • Implement component-specific uninstall handlers for Python, Node.js, and uv that remove their runtime directories and log outcomes.
  • Slightly tidy TypeScript union type formatting in shared types.

Add trash icon uninstall button in component management card,
with confirmation modal. Backend removes the component directory
for Python runtimes, Node.js, and uv.
@Raven95676 Raven95676 linked an issue Jul 3, 2026 that may be closed by this pull request

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In handleComponentUninstall the useCallback dependency array omits handleApiError (and any other non-stable closures it might capture), which can lead to stale references; either include all captured values in the dependency array or avoid useCallback if memoization is not required.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `handleComponentUninstall` the `useCallback` dependency array omits `handleApiError` (and any other non-stable closures it might capture), which can lead to stale references; either include all captured values in the dependency array or avoid `useCallback` if memoization is not required.

## Individual Comments

### Comment 1
<location path="src/pages/Versions.tsx" line_range="153-168" />
<code_context>
     [runOperation, clearDownloadProgress]
   );

+  const handleComponentUninstall = useCallback(async () => {
+    if (!componentToUninstall) return;
+    const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id);
+    await runOperation({
+      key,
+      task: () => api.uninstallComponent(componentToUninstall.id),
+      onSuccess: (result) => {
+        message.success(result);
+      },
+      onError: (error) => {
+        handleApiError(error);
+      },
+    });
+    setComponentUninstallOpen(false);
+    setComponentToUninstall(null);
+  }, [componentToUninstall, runOperation]);
+
   const isInstalled = (tagName: string) => versions.some((v) => v.version === tagName);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider not closing the uninstall modal when the uninstall operation fails

Because `setComponentUninstallOpen(false)` and `setComponentToUninstall(null)` run after `runOperation` regardless of outcome, the modal always closes and the selection is cleared even on failure. This forces the user to reopen the modal to retry and can obscure the error context. Consider only resetting this state on success (e.g., in `onSuccess`) or otherwise distinguishing success vs failure to keep a smoother retry flow.

```suggestion
  const handleComponentUninstall = useCallback(async () => {
    if (!componentToUninstall) return;
    const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id);
    await runOperation({
      key,
      task: () => api.uninstallComponent(componentToUninstall.id),
      onSuccess: (result) => {
        message.success(result);
        setComponentUninstallOpen(false);
        setComponentToUninstall(null);
      },
      onError: (error) => {
        handleApiError(error);
      },
    });
  }, [componentToUninstall, runOperation]);
```
</issue_to_address>

### Comment 2
<location path="src-tauri/src/component/python.rs" line_range="78-90" />
<code_context>
+    let py310_dir = get_python_runtime_dir(RUNTIME_PY310);
+    let py312_dir = get_python_runtime_dir(RUNTIME_PY312);
+
+    let mut removed = Vec::new();
+    for dir in [&py310_dir, &py312_dir] {
+        if dir.exists() {
+            std::fs::remove_dir_all(dir)
+                .map_err(|e| AppError::io(format!("Failed to remove Python runtime: {}", e)))?;
+            removed.push(
+                dir.file_name()
+                    .unwrap_or_default()
+                    .to_string_lossy()
+                    .to_string(),
+            );
+        }
+    }
+
+    if removed.is_empty() {
+        Ok("Python 组件未安装".to_string())
+    } else {
+        Ok(format!("已卸载 Python: {}", removed.join(", ")))
+    }
+}
</code_context>
<issue_to_address>
**suggestion:** Include more specific context in Python uninstall error messages and logs

Since you already compute separate dirs for `RUNTIME_PY310` and `RUNTIME_PY312`, this error should indicate which one failed. Instead of a generic `"Failed to remove Python runtime: {}"`, include the directory or runtime identifier in the message (and logs), e.g. `format!("Failed to remove Python runtime {:?}: {}", dir, e)`, so users can see which specific runtime uninstall failed.

```suggestion
    let mut removed = Vec::new();
    for dir in [&py310_dir, &py312_dir] {
        if dir.exists() {
            std::fs::remove_dir_all(dir).map_err(|e| {
                AppError::io(format!(
                    "Failed to remove Python runtime at {}: {}",
                    dir.display(),
                    e
                ))
            })?;
            removed.push(
                dir.file_name()
                    .unwrap_or_default()
                    .to_string_lossy()
                    .to_string(),
            );
        }
    }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/pages/Versions.tsx
Comment on lines +153 to +168
const handleComponentUninstall = useCallback(async () => {
if (!componentToUninstall) return;
const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id);
await runOperation({
key,
task: () => api.uninstallComponent(componentToUninstall.id),
onSuccess: (result) => {
message.success(result);
},
onError: (error) => {
handleApiError(error);
},
});
setComponentUninstallOpen(false);
setComponentToUninstall(null);
}, [componentToUninstall, runOperation]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider not closing the uninstall modal when the uninstall operation fails

Because setComponentUninstallOpen(false) and setComponentToUninstall(null) run after runOperation regardless of outcome, the modal always closes and the selection is cleared even on failure. This forces the user to reopen the modal to retry and can obscure the error context. Consider only resetting this state on success (e.g., in onSuccess) or otherwise distinguishing success vs failure to keep a smoother retry flow.

Suggested change
const handleComponentUninstall = useCallback(async () => {
if (!componentToUninstall) return;
const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id);
await runOperation({
key,
task: () => api.uninstallComponent(componentToUninstall.id),
onSuccess: (result) => {
message.success(result);
},
onError: (error) => {
handleApiError(error);
},
});
setComponentUninstallOpen(false);
setComponentToUninstall(null);
}, [componentToUninstall, runOperation]);
const handleComponentUninstall = useCallback(async () => {
if (!componentToUninstall) return;
const key = OPERATION_KEYS.uninstallComponent(componentToUninstall.id);
await runOperation({
key,
task: () => api.uninstallComponent(componentToUninstall.id),
onSuccess: (result) => {
message.success(result);
setComponentUninstallOpen(false);
setComponentToUninstall(null);
},
onError: (error) => {
handleApiError(error);
},
});
}, [componentToUninstall, runOperation]);

Comment on lines +78 to +90
let mut removed = Vec::new();
for dir in [&py310_dir, &py312_dir] {
if dir.exists() {
std::fs::remove_dir_all(dir)
.map_err(|e| AppError::io(format!("Failed to remove Python runtime: {}", e)))?;
removed.push(
dir.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Include more specific context in Python uninstall error messages and logs

Since you already compute separate dirs for RUNTIME_PY310 and RUNTIME_PY312, this error should indicate which one failed. Instead of a generic "Failed to remove Python runtime: {}", include the directory or runtime identifier in the message (and logs), e.g. format!("Failed to remove Python runtime {:?}: {}", dir, e), so users can see which specific runtime uninstall failed.

Suggested change
let mut removed = Vec::new();
for dir in [&py310_dir, &py312_dir] {
if dir.exists() {
std::fs::remove_dir_all(dir)
.map_err(|e| AppError::io(format!("Failed to remove Python runtime: {}", e)))?;
removed.push(
dir.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
}
}
let mut removed = Vec::new();
for dir in [&py310_dir, &py312_dir] {
if dir.exists() {
std::fs::remove_dir_all(dir).map_err(|e| {
AppError::io(format!(
"Failed to remove Python runtime at {}: {}",
dir.display(),
e
))
})?;
removed.push(
dir.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
}
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the ability to uninstall core components (Python, Node.js, and uv) from both the backend and frontend. On the backend, it adds the uninstall_component command and specific directory deletion logic for each component. On the frontend, it integrates the API call, adds a confirmation modal, and introduces an uninstall button in the UI. The review feedback highlights two critical issues: first, uninstalling components while active instances are running can cause crashes or file locks, so a check for active instances should be added; second, the uninstall button is not disabled during the uninstallation process, which could allow users to trigger concurrent conflicting operations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src-tauri/src/commands.rs
Comment on lines +325 to +329
ComponentCommandAction::Uninstall => {
tokio::task::spawn_blocking(move || component::uninstall_component(id))
.await
.map_err(|e| AppError::process(format!("Uninstall task panicked: {}", e)))?
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Uninstalling a core component (such as Python, Node.js, or uv) while there are active instances running can lead to application crashes, corrupted environments, or file lock errors (especially on Windows).\n\nIt is highly recommended to check if any instances are currently running and return an error if so, similar to how it is handled in other commands.

        ComponentCommandAction::Uninstall => {\n            if !state.process_manager.get_active_ids().is_empty() {\n                return Err(AppError::instance_running());\n            }\n            tokio::task::spawn_blocking(move || component::uninstall_component(id))\n                .await\n                .map_err(|e| AppError::process(format!(\"Uninstall task panicked: {}\", e)))?\n        }

Comment thread src/pages/Versions.tsx
type="text"
danger
icon={<DeleteOutlined />}
disabled={isComponentOperating}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The isComponentOperating state only checks for isInstalling and isReinstalling. It does not include the isUninstalling state. As a result, the "Uninstall" button (and other action buttons) will not be disabled while the component is being uninstalled, which could allow users to trigger concurrent conflicting operations.\n\nTo prevent this, we should disable the button if an uninstallation is in progress. Ideally, you should also update the definition of isComponentOperating (around line 205) to include the uninstall operation:\n\ntypescript\nconst uninstallKey = OPERATION_KEYS.uninstallComponent(comp.id);\nconst isUninstalling = operations[uninstallKey] || false;\nconst isComponentOperating = isInstalling || isReinstalling || isUninstalling;\n

Suggested change
disabled={isComponentOperating}
disabled={isComponentOperating || (operations[OPERATION_KEYS.uninstallComponent(comp.id)] || false)}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

下载的python,node,uv环境怎么卸载?

1 participant