Skip to content

Commit d822d4a

Browse files
authored
fix(kiloclaw): split upgrade confirmation flow (#2834)
* fix(kiloclaw): split upgrade confirmation flow * fix(kiloclaw): remove extra upgrade analytics
1 parent 0db27fd commit d822d4a

5 files changed

Lines changed: 154 additions & 43 deletions

File tree

apps/web/src/app/(app)/claw/components/ClawInstanceOverview.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import { InstanceControls } from './InstanceControls';
1111
import { InstanceTab } from './InstanceTab';
1212
import { useClawContext } from './ClawContext';
1313

14-
export function ClawInstanceOverview({ status }: { status: KiloClawDashboardStatus }) {
14+
export function ClawInstanceOverview({
15+
status,
16+
onRedeploySuccess,
17+
onRequestUpgrade,
18+
}: {
19+
status: KiloClawDashboardStatus;
20+
onRedeploySuccess?: () => void;
21+
onRequestUpgrade?: () => void;
22+
}) {
1523
const { organizationId } = useClawContext();
1624

1725
const personalMutations = useKiloClawMutations();
@@ -60,6 +68,8 @@ export function ClawInstanceOverview({ status }: { status: KiloClawDashboardStat
6068
<InstanceControls
6169
status={status}
6270
mutations={mutations}
71+
onRedeploySuccess={onRedeploySuccess}
72+
onRequestUpgrade={onRequestUpgrade}
6373
gatewayReady={gatewayStatus?.state === 'running'}
6474
/>
6575
</CardContent>

apps/web/src/app/(app)/claw/components/ClawSettingsPage.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ClawContextProvider, useClawContext } from './ClawContext';
1111
import { ClawConfigServiceBanner } from './ClawConfigServiceBanner';
1212
import { ClawInstanceOverview } from './ClawInstanceOverview';
1313
import { SettingsTab } from './SettingsTab';
14+
import { UpgradeKiloClawDialog } from './UpgradeKiloClawDialog';
1415
import { BillingWrapper } from './billing/BillingWrapper';
1516
import { SetPageTitle } from '@/components/SetPageTitle';
1617
import { Card, CardContent } from '@/components/ui/card';
@@ -33,6 +34,7 @@ function ClawSettingsInner({
3334
const mutations = organizationId ? orgMutations : personalMutations;
3435

3536
const [dirtySecrets, setDirtySecrets] = useState<Set<string>>(new Set());
37+
const [confirmUpgrade, setConfirmUpgrade] = useState(false);
3638
const onSecretsChanged = useCallback((entryId: string) => {
3739
setDirtySecrets(prev => new Set([...prev, entryId]));
3840
}, []);
@@ -53,12 +55,17 @@ function ClawSettingsInner({
5355
});
5456
}, [mutations.restartMachine, onRedeploySuccess]);
5557

56-
const onUpgrade = useCallback(() => {
58+
const onRequestUpgrade = useCallback(() => {
59+
setConfirmUpgrade(true);
60+
}, []);
61+
62+
const onConfirmUpgrade = useCallback(() => {
5763
mutations.restartMachine.mutate(
5864
{ imageTag: 'latest' },
5965
{
6066
onSuccess: () => {
61-
toast.success('Upgrading to latest image');
67+
toast.success('Upgrading KiloClaw');
68+
setConfirmUpgrade(false);
6269
onRedeploySuccess();
6370
},
6471
onError: err => {
@@ -68,21 +75,32 @@ function ClawSettingsInner({
6875
);
6976
}, [mutations.restartMachine, onRedeploySuccess]);
7077

71-
const onRequestUpgrade = useCallback(() => {
72-
onUpgrade();
73-
}, [onUpgrade]);
74-
7578
return (
76-
<SettingsTab
77-
status={status}
78-
mutations={mutations}
79-
onSecretsChanged={onSecretsChanged}
80-
dirtySecrets={dirtySecrets}
81-
onRedeploy={onRedeploy}
82-
onUpgrade={onUpgrade}
83-
onRequestUpgrade={onRequestUpgrade}
84-
organizationName={organizationName}
85-
/>
79+
<>
80+
<ClawInstanceOverview
81+
status={status}
82+
onRedeploySuccess={onRedeploySuccess}
83+
onRequestUpgrade={onRequestUpgrade}
84+
/>
85+
<SettingsTab
86+
status={status}
87+
mutations={mutations}
88+
onSecretsChanged={onSecretsChanged}
89+
dirtySecrets={dirtySecrets}
90+
onRedeploy={onRedeploy}
91+
onRequestUpgrade={onRequestUpgrade}
92+
organizationName={organizationName}
93+
/>
94+
<UpgradeKiloClawDialog
95+
open={confirmUpgrade}
96+
onOpenChange={open => {
97+
if (mutations.restartMachine.isPending) return;
98+
setConfirmUpgrade(open);
99+
}}
100+
isPending={mutations.restartMachine.isPending}
101+
onConfirm={onConfirmUpgrade}
102+
/>
103+
</>
86104
);
87105
}
88106

@@ -139,7 +157,6 @@ function ClawSettingsWithStatus({
139157
const settingsContent = (
140158
<div className="flex flex-col gap-6">
141159
<ClawConfigServiceBanner status={status} />
142-
<ClawInstanceOverview status={status} />
143160
<ClawSettingsInner status={status} organizationName={organizationName} />
144161
</div>
145162
);

apps/web/src/app/(app)/claw/components/InstanceControls.tsx

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { StartKiloCliRunDialog } from './StartKiloCliRunDialog';
3838
import { AnimatedDots } from './AnimatedDots';
3939
import { OpenClawButton } from './OpenClawButton';
4040
import { KiloClawUpdateAvailableBanner } from './KiloClawUpdateAvailableBanner';
41+
import { UpgradeKiloClawDialog } from './UpgradeKiloClawDialog';
4142

4243
const VOLUME_SIZE_GB = 10;
4344
// Default machine spec fallback (matches kiloclaw DEFAULT_MACHINE_GUEST)
@@ -54,15 +55,13 @@ export function InstanceControls({
5455
status,
5556
mutations,
5657
onRedeploySuccess,
57-
upgradeRequested,
58-
onUpgradeHandled,
58+
onRequestUpgrade,
5959
gatewayReady,
6060
}: {
6161
status: KiloClawDashboardStatus;
6262
mutations: ClawMutations;
6363
onRedeploySuccess?: () => void;
64-
upgradeRequested?: boolean;
65-
onUpgradeHandled?: () => void;
64+
onRequestUpgrade?: () => void;
6665
gatewayReady?: boolean;
6766
}) {
6867
const posthog = usePostHog();
@@ -84,6 +83,7 @@ export function InstanceControls({
8483
const [kiloRunOpen, setKiloRunOpen] = useState(false);
8584
const [confirmRestart, setConfirmRestart] = useState(false);
8685
const [confirmRedeploy, setConfirmRedeploy] = useState(false);
86+
const [confirmUpgrade, setConfirmUpgrade] = useState(false);
8787
const [redeployMode, setRedeployMode] = useState<'redeploy' | 'upgrade'>('redeploy');
8888

8989
const { updateAvailable, catalogNewerThanImage, latestAvailableVersion, latestVersion } =
@@ -113,6 +113,33 @@ export function InstanceControls({
113113
setManuallyDismissed(true);
114114
}, [dismissKey]);
115115

116+
const openUpgradeConfirmation = useCallback(() => {
117+
if (onRequestUpgrade) {
118+
onRequestUpgrade();
119+
return;
120+
}
121+
122+
setConfirmUpgrade(true);
123+
}, [onRequestUpgrade]);
124+
125+
const handleUpgradeConfirm = useCallback(() => {
126+
posthog?.capture('claw_redeploy_clicked', {
127+
instance_status: status.status,
128+
redeploy_mode: 'upgrade',
129+
});
130+
mutations.restartMachine.mutate(
131+
{ imageTag: 'latest' },
132+
{
133+
onSuccess: () => {
134+
toast.success('Upgrading KiloClaw');
135+
setConfirmUpgrade(false);
136+
onRedeploySuccess?.();
137+
},
138+
onError: err => toast.error(err.message, { duration: 10000 }),
139+
}
140+
);
141+
}, [mutations.restartMachine, onRedeploySuccess, posthog, status.status]);
142+
116143
const showUpgradeBanner =
117144
isFlyProvider && updateAvailable && !isDismissedInStorage && !manuallyDismissed;
118145

@@ -132,18 +159,6 @@ export function InstanceControls({
132159
);
133160
};
134161

135-
// Toggle-flag pattern: parent sets upgradeRequested=true, we open the dialog
136-
// with "upgrade" preselected, then immediately reset via onUpgradeHandled.
137-
// Safe for single-click flows; won't re-fire if already true (no state change).
138-
useEffect(() => {
139-
if (!upgradeRequested) return;
140-
if (isFlyProvider) {
141-
setRedeployMode('upgrade');
142-
setConfirmRedeploy(true);
143-
}
144-
onUpgradeHandled?.();
145-
}, [upgradeRequested, isFlyProvider, onUpgradeHandled]);
146-
147162
return (
148163
<div>
149164
<div className="flex items-center gap-2">
@@ -216,10 +231,7 @@ export function InstanceControls({
216231
<KiloClawUpdateAvailableBanner
217232
className="mb-4"
218233
catalogNewerThanImage={catalogNewerThanImage}
219-
onUpgrade={() => {
220-
setRedeployMode('upgrade');
221-
setConfirmRedeploy(true);
222-
}}
234+
onUpgrade={openUpgradeConfirmation}
223235
onDismiss={dismissBanner}
224236
/>
225237
)}
@@ -458,6 +470,16 @@ export function InstanceControls({
458470
</DialogFooter>
459471
</DialogContent>
460472
</Dialog>
473+
<UpgradeKiloClawDialog
474+
open={confirmUpgrade}
475+
onOpenChange={open => {
476+
if (mutations.restartMachine.isPending) return;
477+
if (!open) posthog?.capture('claw_redeploy_cancelled');
478+
setConfirmUpgrade(open);
479+
}}
480+
isPending={mutations.restartMachine.isPending}
481+
onConfirm={handleUpgradeConfirm}
482+
/>
461483
<RunDoctorDialog
462484
open={doctorOpen}
463485
onOpenChange={setDoctorOpen}

apps/web/src/app/(app)/claw/components/SettingsTab.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,6 @@ export function SettingsTab({
13051305
onSecretsChanged,
13061306
dirtySecrets,
13071307
onRedeploy,
1308-
onUpgrade,
13091308
onRequestUpgrade,
13101309
organizationName,
13111310
}: {
@@ -1314,9 +1313,7 @@ export function SettingsTab({
13141313
onSecretsChanged?: (entryId: string) => void;
13151314
dirtySecrets: Set<string>;
13161315
onRedeploy?: () => void;
1317-
/** Callback that triggers an image upgrade (pull latest) instead of a plain restart. */
1318-
onUpgrade?: () => void;
1319-
/** Callback that requests an upgrade via the InstanceControls dialog. */
1316+
/** Callback that opens the focused upgrade confirmation flow. */
13201317
onRequestUpgrade?: () => void;
13211318
/** Present in organization context; required in the destroy confirmation phrase. */
13221319
organizationName?: string;
@@ -1879,7 +1876,7 @@ export function SettingsTab({
18791876
mutations={mutations}
18801877
onSecretsChanged={onSecretsChanged}
18811878
isDirty={dirtySecrets.has(entry.id)}
1882-
onRedeploy={onUpgrade ?? onRedeploy}
1879+
onRedeploy={onRequestUpgrade ?? onRedeploy}
18831880
redeployLabel="Upgrade"
18841881
actionRowExtra={<AgentCardSetupGuide />}
18851882
/>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client';
2+
3+
import { ArrowUpCircle, RotateCw } from 'lucide-react';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@/components/ui/dialog';
12+
import { Button } from '@/components/ui/button';
13+
import { AnimatedDots } from './AnimatedDots';
14+
15+
const upgradeButtonClassName =
16+
'border-amber-500/30 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 hover:text-amber-300';
17+
18+
export function UpgradeKiloClawDialog({
19+
open,
20+
onOpenChange,
21+
isPending,
22+
onConfirm,
23+
}: {
24+
open: boolean;
25+
onOpenChange: (open: boolean) => void;
26+
isPending: boolean;
27+
onConfirm: () => void;
28+
}) {
29+
return (
30+
<Dialog open={open} onOpenChange={isPending ? undefined : onOpenChange}>
31+
<DialogContent className="max-w-md gap-5">
32+
<DialogHeader className="gap-3 space-y-0 pr-6">
33+
<div className="flex h-10 w-10 items-center justify-center rounded-full border border-amber-500/30 bg-amber-500/10 text-amber-300">
34+
<ArrowUpCircle className="h-5 w-5" />
35+
</div>
36+
<div className="space-y-2">
37+
<DialogTitle>Upgrade KiloClaw</DialogTitle>
38+
<DialogDescription className="leading-6">
39+
Upgrade this instance to the latest supported KiloClaw version. This also redeploys
40+
the runtime, so it may be briefly offline.
41+
</DialogDescription>
42+
</div>
43+
</DialogHeader>
44+
<DialogFooter className="gap-2 sm:gap-0">
45+
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
46+
Cancel
47+
</Button>
48+
<Button className={upgradeButtonClassName} onClick={onConfirm} disabled={isPending}>
49+
{isPending ? (
50+
<>
51+
Upgrading
52+
<AnimatedDots />
53+
</>
54+
) : (
55+
<>
56+
<RotateCw className="h-4 w-4" />
57+
Upgrade & Redeploy
58+
</>
59+
)}
60+
</Button>
61+
</DialogFooter>
62+
</DialogContent>
63+
</Dialog>
64+
);
65+
}

0 commit comments

Comments
 (0)