Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/app/(app)/claw/components/ClawDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<div className="container m-auto flex w-full max-w-[1140px] flex-col gap-6 p-4 md:p-6">
Expand Down Expand Up @@ -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
</TabsTrigger>
<TabsTrigger
value="settings"
Expand All @@ -66,7 +72,12 @@ export function ClawDashboard({ status }: { status: KiloClawDashboardStatus | un
</div>
<CardContent className="p-5">
<TabsContent value="instance" className="mt-0">
<InstanceTab status={instanceStatus} />
<InstanceTab
status={instanceStatus}
gatewayStatus={gatewayStatus}
gatewayLoading={gatewayLoading}
gatewayError={gatewayError}
/>
</TabsContent>
<TabsContent value="settings" className="mt-0">
<SettingsTab status={instanceStatus} mutations={mutations} />
Expand Down
62 changes: 62 additions & 0 deletions src/app/(app)/claw/components/ConfirmActionDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onOpenChange={isPending ? undefined : onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
Cancel
</Button>
<Button className={className} onClick={onConfirm} disabled={isPending}>
{isPending ? (
pendingLabel
) : (
<>
{confirmIcon}
{confirmLabel}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
103 changes: 83 additions & 20 deletions src/app/(app)/claw/components/InstanceControls.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof useKiloClawMutations>;
Expand All @@ -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 (
<div>
<h3 className="text-foreground mb-1 text-sm font-medium">Instance Controls</h3>
<p className="text-muted-foreground mb-4 text-xs">
Manage power state and gateway lifecycle.
</p>
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h3 className="text-foreground mb-1 text-sm font-medium">Instance Controls</h3>
<p className="text-muted-foreground text-xs">Manage power state and gateway lifecycle.</p>
</div>
<div className="flex flex-wrap justify-end gap-2">
<Badge variant="outline" className="text-muted-foreground gap-1.5 font-normal">
<Cpu className="h-3.5 w-3.5" />
{DEFAULT_CLAW_INSTANCE_TYPE.name} ({DEFAULT_CLAW_INSTANCE_TYPE.description})
</Badge>
<Badge variant="outline" className="text-muted-foreground gap-1.5 font-normal">
<HardDrive className="h-3.5 w-3.5" />
20 GB SSD
</Badge>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
Expand All @@ -42,39 +59,35 @@ export function InstanceControls({
}}
>
<Play className="h-4 w-4" />
{mutations.start.isPending ? 'Starting...' : 'Start'}
{mutations.start.isPending ? 'Starting...' : 'Start Machine'}
</Button>
<Button
size="sm"
variant="outline"
className="border-red-500/30 text-red-400 hover:bg-red-500/10 hover:text-red-300"
disabled={!isRunning || mutations.stop.isPending || isDestroying}
className="border-violet-500/30 text-violet-400 hover:bg-violet-500/10 hover:text-violet-300"
disabled={!isRunning || mutations.restartOpenClaw.isPending || isDestroying}
onClick={() => {
posthog?.capture('claw_stop_instance_clicked', { instance_status: status.status });
mutations.stop.mutate(undefined, {
onSuccess: () => toast.success('Instance stopped'),
onError: err => toast.error(err.message),
posthog?.capture('claw_restart_openclaw_prompted', {
instance_status: status.status,
});
setConfirmRestart(true);
}}
>
<Square className="h-4 w-4" />
{mutations.stop.isPending ? 'Stopping...' : 'Stop'}
<RefreshCw className="h-4 w-4" />
Restart OpenClaw
</Button>
<Button
size="sm"
variant="outline"
className="border-amber-500/30 text-amber-400 hover:bg-amber-500/10 hover:text-amber-300"
disabled={!isRunning || mutations.restartGateway.isPending || isDestroying}
onClick={() => {
posthog?.capture('claw_redeploy_clicked', { instance_status: status.status });
mutations.restartGateway.mutate(undefined, {
onSuccess: () => toast.success('Gateway restarting'),
onError: err => toast.error(err.message),
});
posthog?.capture('claw_redeploy_prompted', { instance_status: status.status });
setConfirmRedeploy(true);
}}
>
<RotateCw className="h-4 w-4" />
{mutations.restartGateway.isPending ? 'Redeploying...' : 'Redeploy'}
Redeploy
</Button>
<Button
size="sm"
Expand All @@ -90,6 +103,56 @@ export function InstanceControls({
OpenClaw Doctor
</Button>
</div>
<ConfirmActionDialog
open={confirmRestart}
onOpenChange={open => {
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={<RefreshCw className="h-4 w-4" />}
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),
});
}}
/>
<ConfirmActionDialog
open={confirmRedeploy}
onOpenChange={open => {
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={<RotateCw className="h-4 w-4" />}
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),
});
}}
/>
<RunDoctorDialog
open={doctorOpen}
onOpenChange={setDoctorOpen}
Expand Down
Loading