From c23a51d53af9f7bd8e3980aa35a9a2ca53451ae6 Mon Sep 17 00:00:00 2001 From: St0rmz1 Date: Tue, 3 Mar 2026 13:42:37 -0800 Subject: [PATCH 1/4] feat(kiloclaw): Add user self-pinning UI and admin improvements (Phase 2.1) fix merge conflicts --- src/app/(app)/claw/components/SettingsTab.tsx | 5 + .../(app)/claw/components/VersionPinCard.tsx | 216 ++++++++++++++ .../KiloclawInstanceDetail.tsx | 136 +++++---- .../KiloclawVersions/KiloclawVersionsPage.tsx | 14 +- src/hooks/useKiloClaw.ts | 39 +++ src/routers/admin-kiloclaw-versions-router.ts | 42 ++- src/routers/kiloclaw-router.ts | 176 +++++++++++- .../kiloclaw-user-versions-router.test.ts | 271 ++++++++++++++++++ 8 files changed, 820 insertions(+), 79 deletions(-) create mode 100644 src/app/(app)/claw/components/VersionPinCard.tsx create mode 100644 src/routers/kiloclaw-user-versions-router.test.ts diff --git a/src/app/(app)/claw/components/SettingsTab.tsx b/src/app/(app)/claw/components/SettingsTab.tsx index a5b58a19b..28c71f444 100644 --- a/src/app/(app)/claw/components/SettingsTab.tsx +++ b/src/app/(app)/claw/components/SettingsTab.tsx @@ -20,6 +20,7 @@ import { DetailTile } from './DetailTile'; import { ChannelTokenInput } from './ChannelTokenInput'; import { CHANNELS, CHANNEL_TYPES, type ChannelDefinition } from './channel-config'; import { ConfirmActionDialog } from './ConfirmActionDialog'; +import { VersionPinCard } from './VersionPinCard'; type ClawMutations = ReturnType; @@ -337,6 +338,10 @@ export function SettingsTab({ + + + +
diff --git a/src/app/(app)/claw/components/VersionPinCard.tsx b/src/app/(app)/claw/components/VersionPinCard.tsx new file mode 100644 index 000000000..f55d148d6 --- /dev/null +++ b/src/app/(app)/claw/components/VersionPinCard.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useState } from 'react'; +import { Pin, PinOff, Info } from 'lucide-react'; +import { toast } from 'sonner'; +import { useKiloClawAvailableVersions, useKiloClawMyPin, useKiloClawMutations } from '@/hooks/useKiloClaw'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; + +export function VersionPinCard() { + const { data: myPin, isLoading: pinLoading } = useKiloClawMyPin(); + const { data: versions, isLoading: versionsLoading } = useKiloClawAvailableVersions(0, 50); + const mutations = useKiloClawMutations(); + + const [selectedImageTag, setSelectedImageTag] = useState(''); + const [reason, setReason] = useState(''); + + const isPinned = !!myPin; + const isLoading = pinLoading || versionsLoading; + const isPinning = mutations.setMyPin.isPending; + const isUnpinning = mutations.removeMyPin.isPending; + + // Self-pin: pinned_by matches the pin's user_id (the user pinned themselves) + // Admin-pin: pinned_by differs from user_id (an admin pinned this user) + const pinnedBySelf = myPin && myPin.pinned_by === myPin.user_id; + const pinnedByLabel = pinnedBySelf ? 'You' : 'Kilo Admin'; + + const handlePin = async () => { + if (!selectedImageTag) { + toast.error('Please select a version to pin'); + return; + } + + try { + await mutations.setMyPin.mutateAsync({ + imageTag: selectedImageTag, + reason: reason.trim() || undefined, + }); + toast.success('Version pinned successfully. Use the "Redeploy or Upgrade" button to apply this version.'); + setSelectedImageTag(''); + setReason(''); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to pin version'; + toast.error(message); + } + }; + + const handleUnpin = async () => { + try { + await mutations.removeMyPin.mutateAsync(); + toast.success('Version pin removed. Use the "Redeploy or Upgrade" button to return to the latest version.'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to remove pin'; + toast.error(message); + } + }; + + if (isLoading) { + return ( +
+

+ + Version Pinning +

+

Loading version information...

+
+ ); + } + + const truncateTag = (tag: string) => { + if (tag.length <= 20) return tag; + return `${tag.slice(0, 8)}...${tag.slice(-8)}`; + }; + + return ( +
+

+ + 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. + +
+
+ + {/* Right: Current Status */} +
+

Current Status

+ {isPinned ? ( +
+
+ Pinned to: + + {myPin.openclaw_version} / {myPin.variant} + +
+
+ Image tag: + + {truncateTag(myPin.image_tag)} + +
+ {myPin.reason && ( +
+ Reason: + {myPin.reason} +
+ )} +
+ Pinned by: + {pinnedByLabel} +
+
+ +

+ + + Unpinning returns to following latest. Use the Upgrade button to upgrade your instance. + +

+
+
+ ) : ( +
+ + Following latest + + + Automatically uses newest version + +
+ )} +
+
+ + {/* Row 3: Pin/Unpin Controls */} + {!isPinned ? ( +
+ {/* Left Column: Version Selector + Reason */} +
+
+ + +
+
+ +