From b7799627eae8ecfbb7dbc8ac060774fdf0bdd503 Mon Sep 17 00:00:00 2001 From: zhixian Date: Thu, 19 Feb 2026 17:47:59 +0900 Subject: [PATCH] feat: in-app OpenClaw upgrade dialog with backup flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old "Backup & Upgrade" button (which opened GitHub releases) with a proper multi-step upgrade dialog: confirm → backup → run upgrade command → done. Works for both local and remote (SSH) instances. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src-tauri/src/commands.rs | 43 +++++++ src-tauri/src/lib.rs | 3 + src/components/UpgradeDialog.tsx | 207 +++++++++++++++++++++++++++++++ src/lib/api.ts | 6 + src/pages/Home.tsx | 50 +++----- 5 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 src/components/UpgradeDialog.tsx diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6b5f4bb..f2f9ac3 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -5368,3 +5368,46 @@ pub async fn remote_refresh_model_catalog( let cfg: Value = serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))?; Ok(collect_model_catalog(&cfg)) } + +#[tauri::command] +pub async fn run_openclaw_upgrade() -> Result { + let output = Command::new("bash") + .args(["-c", "curl -fsSL https://openclaw.ai/install.sh | bash"]) + .output() + .map_err(|e| format!("Failed to run upgrade: {e}"))?; + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let combined = if stderr.is_empty() { + stdout + } else { + format!("{stdout}\n{stderr}") + }; + if output.status.success() { + Ok(combined) + } else { + Err(combined) + } +} + +#[tauri::command] +pub async fn remote_run_openclaw_upgrade( + pool: State<'_, SshConnectionPool>, + host_id: String, +) -> Result { + let result = pool + .exec_login( + &host_id, + "curl -fsSL https://openclaw.ai/install.sh | bash", + ) + .await?; + let combined = if result.stderr.is_empty() { + result.stdout.clone() + } else { + format!("{}\n{}", result.stdout, result.stderr) + }; + if result.exit_code == 0 { + Ok(combined) + } else { + Err(combined) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9364d4b..9fede57 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -31,6 +31,7 @@ use crate::commands::{ remote_extract_model_profiles_from_config, remote_refresh_model_catalog, remote_chat_via_openclaw, remote_check_openclaw_update, remote_save_config_baseline, remote_check_config_dirty, remote_discard_config_changes, remote_apply_pending_changes, + run_openclaw_upgrade, remote_run_openclaw_upgrade, RemoteConfigBaselines, }; use crate::ssh::SshConnectionPool; @@ -146,6 +147,8 @@ pub fn run() { remote_check_config_dirty, remote_discard_config_changes, remote_apply_pending_changes, + run_openclaw_upgrade, + remote_run_openclaw_upgrade, ]) .run(tauri::generate_context!()) .expect("failed to run app"); diff --git a/src/components/UpgradeDialog.tsx b/src/components/UpgradeDialog.tsx new file mode 100644 index 0000000..5d05812 --- /dev/null +++ b/src/components/UpgradeDialog.tsx @@ -0,0 +1,207 @@ +import { useState } from "react"; +import { api } from "../lib/api"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; + +type Step = "confirm" | "backup" | "upgrading" | "done"; + +export function UpgradeDialog({ + open, + onOpenChange, + isRemote, + instanceId, + currentVersion, + latestVersion, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + isRemote: boolean; + instanceId: string; + currentVersion: string; + latestVersion: string; +}) { + const [step, setStep] = useState("confirm"); + const [backupName, setBackupName] = useState(""); + const [output, setOutput] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const reset = () => { + setStep("confirm"); + setBackupName(""); + setOutput(""); + setError(""); + setLoading(false); + }; + + const handleClose = (open: boolean) => { + if (!open && !loading) { + reset(); + onOpenChange(false); + } + }; + + const startUpgrade = async () => { + if (isRemote) { + setStep("upgrading"); + runUpgrade(); + } else { + setStep("backup"); + runBackup(); + } + }; + + const runBackup = async () => { + setLoading(true); + setError(""); + try { + const info = await api.backupBeforeUpgrade(); + setBackupName(info.name); + setLoading(false); + setStep("upgrading"); + runUpgrade(); + } catch (e) { + setError(String(e)); + setLoading(false); + } + }; + + const runUpgrade = async () => { + setLoading(true); + setError(""); + setOutput(""); + try { + const result = isRemote + ? await api.remoteRunOpenclawUpgrade(instanceId) + : await api.runOpenclawUpgrade(); + setOutput(result); + setStep("done"); + } catch (e) { + setOutput(String(e)); + setError("Upgrade failed. See output below."); + } finally { + setLoading(false); + } + }; + + return ( + + + + + {step === "confirm" && "Upgrade OpenClaw"} + {step === "backup" && "Creating Backup..."} + {step === "upgrading" && "Upgrading..."} + {step === "done" && "Upgrade Complete"} + + + + {step === "confirm" && ( +
+
+ Current: + {currentVersion} + + New: + {latestVersion} +
+

+ {isRemote + ? "This will upgrade OpenClaw on the remote instance." + : "This will back up your config and upgrade OpenClaw."} +

+
+ )} + + {step === "backup" && ( +
+ {loading && ( +
+
+ Creating backup... +
+ )} + {error && ( +
{error}
+ )} +
+ )} + + {step === "upgrading" && ( +
+ {backupName && ( +

+ Backup created: {backupName} +

+ )} + {loading && ( +
+
+ Running upgrade... +
+ )} + {error && ( +
{error}
+ )} + {output && ( +
+                {output}
+              
+ )} +
+ )} + + {step === "done" && ( +
+ {backupName && ( +

+ Backup: {backupName} +

+ )} +

+ Upgrade completed successfully. +

+ {output && ( +
+                {output}
+              
+ )} +
+ )} + + + {step === "confirm" && ( + <> + + + + )} + {step === "backup" && error && ( + <> + + + + )} + {step === "upgrading" && error && ( + + )} + {step === "done" && ( + + )} + + +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index a49f572..8d879e0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -200,4 +200,10 @@ export const api = { invoke("remote_discard_config_changes", { hostId }), remoteApplyPendingChanges: (hostId: string): Promise => invoke("remote_apply_pending_changes", { hostId }), + + // Upgrade + runOpenclawUpgrade: (): Promise => + invoke("run_openclaw_upgrade", {}), + remoteRunOpenclawUpgrade: (hostId: string): Promise => + invoke("remote_run_openclaw_upgrade", { hostId }), }; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index ddd8972..9d08d32 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -24,6 +24,7 @@ import { AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { CreateAgentDialog } from "@/components/CreateAgentDialog"; +import { UpgradeDialog } from "@/components/UpgradeDialog"; import { RecipeCard } from "@/components/RecipeCard"; import { Skeleton } from "@/components/ui/skeleton"; import type { StatusLight, AgentOverview, Recipe, BackupInfo, ModelProfile, RemoteSystemStatus } from "../lib/types"; @@ -81,6 +82,7 @@ export function Home({ // Create agent dialog const [showCreateAgent, setShowCreateAgent] = useState(false); + const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); // Health status with grace period: retry quickly when unhealthy, then slow-poll const [statusSettled, setStatusSettled] = useState(false); @@ -286,37 +288,13 @@ export function Home({ > View - {isRemote ? ( - - ) : ( - - )} - {backupMessage && ( - {backupMessage} - )} + )} @@ -625,6 +603,16 @@ export function Home({ modelProfiles={modelProfiles} onCreated={() => refreshAgents()} /> + + {/* Upgrade Dialog */} + ); }