Skip to content
Open
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
90 changes: 52 additions & 38 deletions apps/web/src/app/(app)/claw/components/InstanceControls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
ArrowUpCircle,
Check,
Expand Down Expand Up @@ -32,6 +33,8 @@ import {
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import type { useKiloClawMutations } from '@/hooks/useKiloClaw';
import { selectCurrentCliRun } from '@/lib/kiloclaw/cli-run-selection';
import { useClawKiloCliRunHistory } from '../hooks/useClawHooks';
import { useClawUpdateAvailable } from '../hooks/useClawUpdateAvailable';
import { useGatewayUrl } from '../hooks/useGatewayUrl';
import { ConfirmActionDialog } from './ConfirmActionDialog';
Expand Down Expand Up @@ -66,6 +69,7 @@ export function InstanceControls({
onUpgradeHandled?: () => void;
gatewayReady?: boolean;
}) {
const router = useRouter();
const posthog = usePostHog();
const gatewayUrl = useGatewayUrl(status);
const isRunning = status.status === 'running';
Expand All @@ -86,7 +90,7 @@ export function InstanceControls({
const [confirmRestart, setConfirmRestart] = useState(false);
const [confirmRedeploy, setConfirmRedeploy] = useState(false);
const [redeployMode, setRedeployMode] = useState<'redeploy' | 'upgrade'>('redeploy');

const kiloCliRunHistory = useClawKiloCliRunHistory(isRunning);
const { updateAvailable, catalogNewerThanImage, latestAvailableVersion, latestVersion } =
useClawUpdateAvailable(status);

Expand Down Expand Up @@ -213,41 +217,41 @@ export function InstanceControls({
</Badge>
</div>
</div>
{showUpgradeBanner && (
<Banner color="amber" className="mb-4">
<Banner.Icon>
<ArrowUpCircle />
</Banner.Icon>
<Banner.Content>
<Banner.Title>
{catalogNewerThanImage
? `A newer OpenClaw version (${latestAvailableVersion}) is available`
: `A newer image (${latestVersion?.imageTag ?? 'unknown'}) is available`}
</Banner.Title>
<Banner.Description>
Upgrade your instance to get the latest features and fixes.
</Banner.Description>
</Banner.Content>
<Banner.Button
className="text-white"
onClick={() => {
setRedeployMode('upgrade');
setConfirmRedeploy(true);
}}
>
Upgrade now
</Banner.Button>
<button
type="button"
onClick={dismissBanner}
className="text-amber-400/60 hover:text-amber-400 transition-colors"
aria-label="Dismiss upgrade banner"
>
<X className="h-4 w-4" />
</button>
</Banner>
)}
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
<div className="mt-4 flex flex-wrap gap-2">
{showUpgradeBanner && (
<Banner color="amber" className="mb-2 w-full">
<Banner.Icon>
<ArrowUpCircle />
</Banner.Icon>
<Banner.Content>
<Banner.Title>
{catalogNewerThanImage
? `A newer OpenClaw version (${latestAvailableVersion}) is available`
: `A newer image (${latestVersion?.imageTag ?? 'unknown'}) is available`}
</Banner.Title>
<Banner.Description>
Upgrade your instance to get the latest features and fixes.
</Banner.Description>
</Banner.Content>
<Banner.Button
className="text-white"
onClick={() => {
setRedeployMode('upgrade');
setConfirmRedeploy(true);
}}
>
Upgrade now
</Banner.Button>
<button
type="button"
onClick={dismissBanner}
className="transition-colors text-amber-400/60 hover:text-amber-400"
aria-label="Dismiss upgrade banner"
>
<X className="h-4 w-4" />
</button>
</Banner>
)}
<OpenClawButton
canShow={isRunning && !!gatewayReady}
gatewayUrl={gatewayUrl}
Expand All @@ -261,8 +265,8 @@ export function InstanceControls({
disabled={
!isStartable ||
mutations.start.isPending ||
isAutoStarting ||
isDestroying ||
isAutoStarting ||
isStarting ||
isRestarting ||
isRecovering
Expand Down Expand Up @@ -353,8 +357,18 @@ export function InstanceControls({
variant="outline"
className="border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300"
disabled={!isRunning || isDestroying || isStarting || isRestarting || isRecovering}
onClick={() => {
onClick={async () => {
posthog?.capture('claw_kilo_run_clicked', { instance_status: status.status });
try {
const { data } = await kiloCliRunHistory.refetch();
const latestRun = selectCurrentCliRun(data?.runs, status.instanceId);
if (latestRun?.status === 'running') {
router.push(`/claw/kilo-cli-run/${latestRun.id}`);
return;
}
} catch {
// Best-effort pre-check; fall through to open the dialog
}
setKiloRunOpen(true);
}}
>
Expand Down
23 changes: 12 additions & 11 deletions apps/web/src/app/(app)/claw/components/StartKiloCliRunDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ import { useKiloClawMutations } from '@/hooks/useKiloClaw';
import type { PlatformStatusResponse } from '@/lib/kiloclaw/types';
import { AnimatedDots } from './AnimatedDots';

function getErrorUpstreamCode(error: unknown): string | undefined {
if (typeof error !== 'object' || error === null || !('data' in error)) return undefined;
const { data } = error;
if (typeof data !== 'object' || data === null || !('upstreamCode' in data)) return undefined;
return typeof data.upstreamCode === 'string' ? data.upstreamCode : undefined;
}

function isNeedsRedeployError(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'data' in error &&
typeof (error as { data?: unknown }).data === 'object' &&
(error as { data?: { upstreamCode?: unknown } }).data !== null &&
(error as { data: { upstreamCode?: unknown } }).data.upstreamCode ===
'controller_route_unavailable'
);
return getErrorUpstreamCode(error) === 'controller_route_unavailable';
}

export function StartKiloCliRunDialog({
Expand Down Expand Up @@ -67,6 +66,8 @@ export function StartKiloCliRunDialog({
onOpenChange(false);
router.push(`/claw/kilo-cli-run/${data.id}`);
},
onError: err =>
toast.error(err.message || 'Failed to start recovery run', { duration: 10000 }),
}
);
};
Expand Down Expand Up @@ -168,7 +169,7 @@ export function StartKiloCliRunDialog({
onKeyDown={e => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleStart();
void handleStart();
}
}}
/>
Expand All @@ -183,7 +184,7 @@ export function StartKiloCliRunDialog({
Cancel
</Button>
<Button
onClick={handleStart}
onClick={() => void handleStart()}
disabled={!machineReady || !prompt.trim() || startMutation.isPending}
className="bg-emerald-600 text-white hover:bg-emerald-700"
>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/(app)/claw/kilo-cli-run/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function KiloCliRunPage() {
icon={<KiloCrabIcon className="text-muted-foreground h-4 w-4" />}
/>
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => router.push('/claw')}>
<Button variant="ghost" size="sm" onClick={() => router.push('/claw/settings')}>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routes the user back to the settings page instead of the chat page

<ArrowLeft className="h-4 w-4" />
Back to Dashboard
</Button>
Expand Down Expand Up @@ -128,7 +128,7 @@ export default function KiloCliRunPage() {
<p className="text-sm text-red-400">
{statusQuery.error?.message ?? 'Failed to load run status'}
</p>
<Button variant="outline" size="sm" onClick={() => router.push('/claw')}>
<Button variant="outline" size="sm" onClick={() => router.push('/claw/settings')}>
Back to Dashboard
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import {
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { cn } from '@/lib/utils';
import { stripAnsi } from '@/lib/stripAnsi';

const PAGE_SIZE = 25;

type RunStatus = 'all' | 'running' | 'completed' | 'failed' | 'cancelled';
type InitiatedBy = 'all' | 'admin' | 'user';

function StatusBadge({ status }: { status: string }) {
switch (status) {
Expand Down Expand Up @@ -65,12 +67,6 @@ function StatusBadge({ status }: { status: string }) {
}
}

/** Strip ANSI escape codes for display in browser. */
function stripAnsi(raw: string): string {
// eslint-disable-next-line no-control-regex
return raw.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
}

function formatDuration(start: string, end: string): string {
const ms = new Date(end).getTime() - new Date(start).getTime();
if (ms < 1000) return `${ms}ms`;
Expand All @@ -87,6 +83,7 @@ export function CliRunsTab() {
const [search, setSearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<RunStatus>('all');
const [initiatedByFilter, setInitiatedByFilter] = useState<InitiatedBy>('all');
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);

const [debounceTimer, setDebounceTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
Expand All @@ -108,6 +105,7 @@ export function CliRunsTab() {
limit: PAGE_SIZE,
search: debouncedSearch || undefined,
status: statusFilter,
initiatedBy: initiatedByFilter,
},
{ staleTime: 10_000 }
)
Expand Down Expand Up @@ -145,6 +143,22 @@ export function CliRunsTab() {
<SelectItem value="cancelled">Cancelled</SelectItem>
</SelectContent>
</Select>
<Select
value={initiatedByFilter}
onValueChange={v => {
setInitiatedByFilter(v as InitiatedBy);
setPage(0);
}}
>
<SelectTrigger className="w-[150px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All users</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
{pagination && (
<span className="text-muted-foreground ml-auto text-sm">
Expand Down Expand Up @@ -172,9 +186,19 @@ export function CliRunsTab() {
)}
>
<div className="flex items-center justify-between gap-2">
<span className="truncate font-mono text-xs">
{run.user_email ?? run.user_id}
</span>
<div className="flex min-w-0 items-center gap-1.5">
<span className="truncate font-mono text-xs">
{run.user_email ?? run.user_id}
</span>
{run.initiated_by_admin_id && (
<Badge
variant="outline"
className="shrink-0 border-purple-500/30 text-purple-400"
>
Admin
</Badge>
)}
</div>
<StatusBadge status={run.status} />
</div>
<p className="text-muted-foreground truncate text-xs">
Expand Down Expand Up @@ -252,6 +276,8 @@ function RunDetail({
id: string;
user_id: string;
user_email: string | null;
initiated_by_admin_id: string | null;
initiated_by_admin_email: string | null;
prompt: string;
status: string;
exit_code: number | null;
Expand All @@ -273,6 +299,11 @@ function RunDetail({
<span className="truncate font-mono text-xs">{run.user_email ?? run.user_id}</span>
<StatusBadge status={run.status} />
</div>
{run.initiated_by_admin_id && (
<p className="text-muted-foreground text-xs">
Initiated by {run.initiated_by_admin_email ?? 'Admin'}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useful info at a glance for us internally. Will not be available for users.

</p>
)}
<p className="text-sm">{run.prompt}</p>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<span>Started {formatDistanceToNow(new Date(run.started_at), { addSuffix: true })}</span>
Expand Down
Loading
Loading