feat: add component uninstall support#77
Conversation
Add trash icon uninstall button in component management card, with confirmation modal. Backend removes the component directory for Python runtimes, Node.js, and uv.
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
handleComponentUninstalltheuseCallbackdependency array omitshandleApiError(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 avoiduseCallbackif 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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]); |
There was a problem hiding this comment.
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.
| 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]); |
| 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(), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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(), | |
| ); | |
| } | |
| } |
There was a problem hiding this comment.
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.
| ComponentCommandAction::Uninstall => { | ||
| tokio::task::spawn_blocking(move || component::uninstall_component(id)) | ||
| .await | ||
| .map_err(|e| AppError::process(format!("Uninstall task panicked: {}", e)))? | ||
| } |
There was a problem hiding this comment.
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 }| type="text" | ||
| danger | ||
| icon={<DeleteOutlined />} | ||
| disabled={isComponentOperating} |
There was a problem hiding this comment.
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
| disabled={isComponentOperating} | |
| disabled={isComponentOperating || (operations[OPERATION_KEYS.uninstallComponent(comp.id)] || false)} |
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:
Enhancements: