diff --git a/src/app/(app)/claw/components/ClawDashboard.tsx b/src/app/(app)/claw/components/ClawDashboard.tsx index 3538fcc2e..418a964d7 100644 --- a/src/app/(app)/claw/components/ClawDashboard.tsx +++ b/src/app/(app)/claw/components/ClawDashboard.tsx @@ -75,6 +75,10 @@ export function ClawDashboard({ const onSecretsChanged = useCallback((entryId: string) => { setDirtySecrets(prev => new Set([...prev, entryId])); }, []); + const [upgradeRequested, setUpgradeRequested] = useState(false); + const onRequestUpgrade = useCallback(() => setUpgradeRequested(true), []); + const onUpgradeHandled = useCallback(() => setUpgradeRequested(false), []); + const onRedeploySuccess = useCallback(() => { setDirtySecrets(new Set()); }, []); @@ -236,6 +240,8 @@ export function ClawDashboard({ status={instanceStatus} mutations={mutations} onRedeploySuccess={onRedeploySuccess} + upgradeRequested={upgradeRequested} + onUpgradeHandled={onUpgradeHandled} /> @@ -276,6 +282,7 @@ export function ClawDashboard({ mutations={mutations} onSecretsChanged={onSecretsChanged} dirtySecrets={dirtySecrets} + onRequestUpgrade={onRequestUpgrade} /> diff --git a/src/app/(app)/claw/components/InstanceControls.tsx b/src/app/(app)/claw/components/InstanceControls.tsx index a75625027..b02d55c41 100644 --- a/src/app/(app)/claw/components/InstanceControls.tsx +++ b/src/app/(app)/claw/components/InstanceControls.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Cpu, HardDrive, Play, RefreshCw, RotateCw, Stethoscope } from 'lucide-react'; import { usePostHog } from 'posthog-js/react'; import { toast } from 'sonner'; @@ -37,10 +37,14 @@ export function InstanceControls({ status, mutations, onRedeploySuccess, + upgradeRequested, + onUpgradeHandled, }: { status: KiloClawDashboardStatus; mutations: ClawMutations; onRedeploySuccess?: () => void; + upgradeRequested?: boolean; + onUpgradeHandled?: () => void; }) { const posthog = usePostHog(); const isRunning = status.status === 'running'; @@ -57,6 +61,17 @@ export function InstanceControls({ const [confirmRedeploy, setConfirmRedeploy] = useState(false); const [redeployMode, setRedeployMode] = useState<'redeploy' | 'upgrade'>('redeploy'); + // Toggle-flag pattern: parent sets upgradeRequested=true, we open the dialog + // with "upgrade" preselected, then immediately reset via onUpgradeHandled. + // Safe for single-click flows; won't re-fire if already true (no state change). + useEffect(() => { + if (upgradeRequested) { + setRedeployMode('upgrade'); + setConfirmRedeploy(true); + onUpgradeHandled?.(); + } + }, [upgradeRequested, onUpgradeHandled]); + return (
diff --git a/src/app/(app)/claw/components/SettingsTab.tsx b/src/app/(app)/claw/components/SettingsTab.tsx index 43df6f578..78a362fc4 100644 --- a/src/app/(app)/claw/components/SettingsTab.tsx +++ b/src/app/(app)/claw/components/SettingsTab.tsx @@ -347,11 +347,13 @@ export function SettingsTab({ mutations, onSecretsChanged, dirtySecrets, + onRequestUpgrade, }: { status: KiloClawDashboardStatus; mutations: ClawMutations; onSecretsChanged?: (entryId: string) => void; dirtySecrets: Set; + onRequestUpgrade?: () => void; }) { const posthog = usePostHog(); const { data: config } = useKiloClawConfig(); @@ -459,14 +461,27 @@ export function SettingsTab({ !!latestAvailableVersion && latestAvailableVersion !== trackedVersion && calverAtLeast(latestAvailableVersion, trackedVersion); - const updateAvailable = - catalogNewerThanImage && - (!isModified || - (!!runningVersion && - calverAtLeast(latestAvailableVersion, runningVersion) && - latestAvailableVersion !== runningVersion)); const isPinned = !!myPin; const hasVersionInfo = isRunning && trackedVersion && trackedVersion !== ':latest'; + // Only compare image tags when variants match — latestVersion is always + // for the "default" variant, so skip for non-default instances to avoid + // false "Update available" badges that would switch their variant. + const variantsMatch = + !status.imageVariant || + status.imageVariant === 'default' || + status.imageVariant === latestVersion?.variant; + const imageTagDiffers = + hasVersionInfo && + variantsMatch && + !!status.trackedImageTag && + !!latestVersion?.imageTag && + status.trackedImageTag !== latestVersion.imageTag; + const updateAvailable = catalogNewerThanImage + ? !isModified || + (!!runningVersion && + calverAtLeast(latestAvailableVersion, runningVersion) && + latestAvailableVersion !== runningVersion) + : imageTagDiffers; return (
@@ -519,17 +534,20 @@ export function SettingsTab({ {updateAvailable && ( - - Update available - +

- A newer OpenClaw version ({latestAvailableVersion}) is available — - redeploy to upgrade + {catalogNewerThanImage + ? `A newer OpenClaw version (${latestAvailableVersion}) is available — click to upgrade` + : `A newer image (${latestVersion?.imageTag ?? 'unknown'}) is available — click to upgrade`}

@@ -564,7 +582,10 @@ export function SettingsTab({ {/* Expandable version pinning */} {manageVersionOpen && (
- +
)}
diff --git a/src/app/(app)/claw/components/VersionPinCard.tsx b/src/app/(app)/claw/components/VersionPinCard.tsx index f0f8b832e..c99d45d0f 100644 --- a/src/app/(app)/claw/components/VersionPinCard.tsx +++ b/src/app/(app)/claw/components/VersionPinCard.tsx @@ -19,7 +19,13 @@ import { import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -export function VersionPinCard() { +export function VersionPinCard({ + trackedImageTag, + latestImageTag, +}: { + trackedImageTag: string | null; + latestImageTag: string | null; +}) { const { data: myPin, isLoading: pinLoading } = useKiloClawMyPin(); const { data: versions, isLoading: versionsLoading } = useKiloClawAvailableVersions(0, 50); const mutations = useKiloClawMutations(); @@ -94,20 +100,76 @@ export function VersionPinCard() { Version Pinning - {/* Description + Current Status */} -
- {/* Left: Description + Info */} -
-

- Pin your instance to a specific OpenClaw version or follow the latest -

-
- - - Pinning locks your instance to a specific version. You won't receive automatic - updates until you unpin. - +
+ {/* Left: Description + Pinning Controls */} +
+
+

+ Pin your instance to a specific OpenClaw version or follow the latest +

+
+ + + Pinning locks your instance to a specific version. You won't receive automatic + updates until you unpin. + +
+ + {!isPinned ? ( +
+
+ +
+ + +
+
+
+ +