diff --git a/src/app/(app)/claw/components/ClawDashboard.tsx b/src/app/(app)/claw/components/ClawDashboard.tsx index 8b81798f4e..345835c7f9 100644 --- a/src/app/(app)/claw/components/ClawDashboard.tsx +++ b/src/app/(app)/claw/components/ClawDashboard.tsx @@ -1,7 +1,7 @@ 'use client'; import type { KiloClawDashboardStatus } from '@/lib/kiloclaw/types'; -import { useKiloClawMutations } from '@/hooks/useKiloClaw'; +import { useKiloClawGatewayStatus, useKiloClawMutations } from '@/hooks/useKiloClaw'; import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useGatewayUrl } from '../hooks/useGatewayUrl'; @@ -27,6 +27,12 @@ export function ClawDashboard({ status }: { status: KiloClawDashboardStatus | un const mutations = useKiloClawMutations(); const gatewayUrl = useGatewayUrl(status); const instanceStatus = hasPopulatedStatus(status) ? status : null; + const isRunning = instanceStatus?.status === 'running'; + const { + data: gatewayStatus, + isLoading: gatewayLoading, + error: gatewayError, + } = useKiloClawGatewayStatus(isRunning); return (
@@ -54,7 +60,7 @@ export function ClawDashboard({ status }: { status: KiloClawDashboardStatus | un value="instance" className="text-muted-foreground hover:text-foreground data-[state=active]:border-foreground data-[state=active]:text-foreground rounded-none border-b-2 border-transparent px-0 py-3 text-sm font-medium transition-colors data-[state=active]:border-0 data-[state=active]:border-b-2 data-[state=active]:bg-transparent data-[state=active]:shadow-none" > - Instance + Gateway Process - + diff --git a/src/app/(app)/claw/components/ConfirmActionDialog.tsx b/src/app/(app)/claw/components/ConfirmActionDialog.tsx new file mode 100644 index 0000000000..0a47c4689b --- /dev/null +++ b/src/app/(app)/claw/components/ConfirmActionDialog.tsx @@ -0,0 +1,62 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; + +export function ConfirmActionDialog({ + open, + onOpenChange, + title, + description, + confirmLabel, + confirmIcon, + isPending, + pendingLabel, + onConfirm, + className, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmLabel: string; + confirmIcon?: ReactNode; + isPending: boolean; + pendingLabel: string; + onConfirm: () => void; + className?: string; +}) { + return ( + + + + {title} + {description} + + + + + + + + ); +} diff --git a/src/app/(app)/claw/components/InstanceControls.tsx b/src/app/(app)/claw/components/InstanceControls.tsx index e9a2bcbe97..0400c10ffb 100644 --- a/src/app/(app)/claw/components/InstanceControls.tsx +++ b/src/app/(app)/claw/components/InstanceControls.tsx @@ -1,12 +1,15 @@ 'use client'; import { useState } from 'react'; -import { Play, RotateCw, Square, Stethoscope } from 'lucide-react'; +import { Cpu, HardDrive, Play, RefreshCw, RotateCw, Stethoscope } from 'lucide-react'; import { usePostHog } from 'posthog-js/react'; import { toast } from 'sonner'; import type { KiloClawDashboardStatus } from '@/lib/kiloclaw/types'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import type { useKiloClawMutations } from '@/hooks/useKiloClaw'; +import { DEFAULT_CLAW_INSTANCE_TYPE } from './claw.types'; +import { ConfirmActionDialog } from './ConfirmActionDialog'; import { RunDoctorDialog } from './RunDoctorDialog'; type ClawMutations = ReturnType; @@ -23,13 +26,27 @@ export function InstanceControls({ const isStopped = status.status === 'stopped' || status.status === 'provisioned'; const isDestroying = status.status === 'destroying'; const [doctorOpen, setDoctorOpen] = useState(false); + const [confirmRestart, setConfirmRestart] = useState(false); + const [confirmRedeploy, setConfirmRedeploy] = useState(false); return (
-

Instance Controls

-

- Manage power state and gateway lifecycle. -

+
+
+

Instance Controls

+

Manage power state and gateway lifecycle.

+
+
+ + + {DEFAULT_CLAW_INSTANCE_TYPE.name} ({DEFAULT_CLAW_INSTANCE_TYPE.description}) + + + + 20 GB SSD + +
+
+ { + if (!open) posthog?.capture('claw_restart_openclaw_cancelled'); + setConfirmRestart(open); + }} + title="Restart OpenClaw" + description="This will restart the gateway process inside the running machine. Active sessions will be briefly interrupted and reconnect automatically." + confirmLabel="Restart" + confirmIcon={} + isPending={mutations.restartOpenClaw.isPending} + pendingLabel="Restarting..." + className="border-violet-500/30 bg-violet-500/10 text-violet-400 hover:bg-violet-500/20 hover:text-violet-300" + onConfirm={() => { + posthog?.capture('claw_restart_openclaw_clicked', { + instance_status: status.status, + }); + mutations.restartOpenClaw.mutate(undefined, { + onSuccess: () => { + toast.success('OpenClaw restarting'); + setConfirmRestart(false); + }, + onError: err => toast.error(err.message), + }); + }} + /> + { + if (!open) posthog?.capture('claw_redeploy_cancelled'); + setConfirmRedeploy(open); + }} + title="Redeploy Gateway" + description="This will stop the machine, rebuild environment variables and secrets, apply any pending image updates, and restart it. The machine will be briefly offline." + confirmLabel="Redeploy" + confirmIcon={} + isPending={mutations.restartGateway.isPending} + pendingLabel="Redeploying..." + className="border-amber-500/30 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 hover:text-amber-300" + onConfirm={() => { + posthog?.capture('claw_redeploy_clicked', { instance_status: status.status }); + mutations.restartGateway.mutate(undefined, { + onSuccess: () => { + toast.success('Gateway restarting'); + setConfirmRedeploy(false); + }, + onError: err => toast.error(err.message), + }); + }} + /> = { + running: { + label: 'Running', + className: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-400', + }, + stopped: { + label: 'Stopped', + className: 'border-red-500/30 bg-red-500/15 text-red-400', + }, + starting: { + label: 'Starting', + className: 'border-blue-500/30 bg-blue-500/15 text-blue-400', + }, + stopping: { + label: 'Stopping', + className: 'border-amber-500/30 bg-amber-500/15 text-amber-400', + }, + crashed: { + label: 'Crashed', + className: 'border-red-500/30 bg-red-500/15 text-red-400', + }, + shutting_down: { + label: 'Shutting Down', + className: 'border-amber-500/30 bg-amber-500/15 text-amber-400 animate-pulse', + }, +}; - return ( -
-
- {details.map(detail => ( - - ))} +function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const mins = Math.floor((seconds % 3600) / 60); + if (days > 0) return `${days}d ${hours}h ${mins}m`; + if (hours > 0) return `${hours}h ${mins}m`; + return `${mins}m`; +} + +function formatLastExit(lastExit: NonNullable): string { + const code = lastExit.code ?? 'null'; + const signal = lastExit.signal ?? 'none'; + const at = new Date(lastExit.at); + const timeStr = at.toLocaleString(); + return `exit ${code} / ${signal} at ${timeStr}`; +} + +export function InstanceTab({ + status, + gatewayStatus, + gatewayLoading, + gatewayError, +}: { + status: KiloClawDashboardStatus; + gatewayStatus: GatewayProcessStatusResponse | undefined; + gatewayLoading: boolean; + gatewayError: { message: string } | null; +}) { + const isRunning = status.status === 'running'; + + if (!isRunning) { + return ( +

+ Gateway status is available when the machine is running. +

+ ); + } + + if (gatewayLoading) { + return ( +
+ + Loading gateway status...
+ ); + } - + if (gatewayError) { + return ( +

+ Failed to load gateway status: {gatewayError.message} +

+ ); + } -
- - - + if (!gatewayStatus) return null; + + const stateStyle = GATEWAY_STATE_STYLES[gatewayStatus.state]; + + return ( +
+
+

State

+ + + {stateStyle.label} + +
+
+

Uptime

+

{formatUptime(gatewayStatus.uptime)}

+
+
+

Restarts

+

{gatewayStatus.restarts}

+
+
+

Last Exit

+

+ {gatewayStatus.lastExit ? formatLastExit(gatewayStatus.lastExit) : '—'} +

+
+
+

Provisioned

+

{formatTs(status.provisionedAt)}

); diff --git a/src/app/(app)/claw/components/SettingsTab.tsx b/src/app/(app)/claw/components/SettingsTab.tsx index 8ca49fe3ba..56d6b437b9 100644 --- a/src/app/(app)/claw/components/SettingsTab.tsx +++ b/src/app/(app)/claw/components/SettingsTab.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { AlertTriangle, Save, Square, X } from 'lucide-react'; +import { AlertTriangle, Hash, Save, Square, X } from 'lucide-react'; import { usePostHog } from 'posthog-js/react'; import { toast } from 'sonner'; import type { KiloClawDashboardStatus } from '@/lib/kiloclaw/types'; @@ -12,6 +12,7 @@ import { ModelCombobox, type ModelOption } from '@/components/shared/ModelCombob import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; +import { DetailTile } from './DetailTile'; import { useDefaultModelSelection } from '../hooks/useDefaultModelSelection'; import { ChannelTokenInput } from './ChannelTokenInput'; import { CHANNELS, CHANNEL_TYPES, type ChannelDefinition } from './channel-config'; @@ -197,6 +198,14 @@ export function SettingsTab({ return (
+
+ + + +
+ + +

KiloCode Configuration

diff --git a/src/app/(app)/claw/components/changelog-data.ts b/src/app/(app)/claw/components/changelog-data.ts index 9cba7e39fd..64872a8b58 100644 --- a/src/app/(app)/claw/components/changelog-data.ts +++ b/src/app/(app)/claw/components/changelog-data.ts @@ -10,6 +10,13 @@ export type ChangelogEntry = { // Newest entries first. Developers add new entries to the top of this array. export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ + { + date: '2026-02-23', + description: + 'Redesigned dashboard: live gateway process status, added ability to restart OpenClaw gateway.', + category: 'feature', + deployHint: null, + }, { date: '2026-02-23', description: 'Deploy OpenClaw 2026.2.22. Added device pairing support to the dashboard.', diff --git a/src/app/(app)/claw/components/claw.types.ts b/src/app/(app)/claw/components/claw.types.ts index cf00891214..14ea19a639 100644 --- a/src/app/(app)/claw/components/claw.types.ts +++ b/src/app/(app)/claw/components/claw.types.ts @@ -34,11 +34,11 @@ export const CLAW_STATUS_BADGE: Record< { label: string; className: string } > = { running: { - label: 'Running', + label: 'Machine Online', className: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-400', }, stopped: { - label: 'Stopped', + label: 'Machine Stopped', className: 'border-red-500/30 bg-red-500/15 text-red-400', }, provisioned: { diff --git a/src/hooks/useKiloClaw.ts b/src/hooks/useKiloClaw.ts index 09754b1930..a3a9ace985 100644 --- a/src/hooks/useKiloClaw.ts +++ b/src/hooks/useKiloClaw.ts @@ -61,6 +61,16 @@ export function useRefreshDevicePairing() { }; } +export function useKiloClawGatewayStatus(enabled: boolean) { + const trpc = useTRPC(); + return useQuery( + trpc.kiloclaw.gatewayStatus.queryOptions(undefined, { + enabled, + refetchInterval: enabled ? 30_000 : false, + }) + ); +} + export function useKiloClawMutations() { const trpc = useTRPC(); const queryClient = useQueryClient(); @@ -93,7 +103,24 @@ export function useKiloClawMutations() { }) ), restartGateway: useMutation( - trpc.kiloclaw.restartGateway.mutationOptions({ onSuccess: invalidateStatus }) + trpc.kiloclaw.restartGateway.mutationOptions({ + onSuccess: async () => { + await invalidateStatus(); + await queryClient.invalidateQueries({ + queryKey: trpc.kiloclaw.gatewayStatus.queryKey(), + }); + }, + }) + ), + restartOpenClaw: useMutation( + trpc.kiloclaw.restartOpenClaw.mutationOptions({ + onSuccess: async () => { + await invalidateStatus(); + await queryClient.invalidateQueries({ + queryKey: trpc.kiloclaw.gatewayStatus.queryKey(), + }); + }, + }) ), approvePairingRequest: useMutation( trpc.kiloclaw.approvePairingRequest.mutationOptions({ diff --git a/src/routers/kiloclaw-router.ts b/src/routers/kiloclaw-router.ts index d3f1387dd1..5449fbbd0d 100644 --- a/src/routers/kiloclaw-router.ts +++ b/src/routers/kiloclaw-router.ts @@ -287,6 +287,16 @@ export const kiloclawRouter = createTRPCRouter({ return client.approveDevicePairingRequest(ctx.user.id, input.requestId); }), + gatewayStatus: kiloclawProcedure.query(async ({ ctx }) => { + const client = new KiloClawInternalClient(); + return client.getGatewayStatus(ctx.user.id); + }), + + restartOpenClaw: kiloclawProcedure.mutation(async ({ ctx }) => { + const client = new KiloClawInternalClient(); + return client.restartGatewayProcess(ctx.user.id); + }), + runDoctor: kiloclawProcedure.mutation(async ({ ctx }) => { const client = new KiloClawInternalClient(); return client.runDoctor(ctx.user.id);