Files
kandev/apps/web/components/session/prepare-progress.tsx
T
2026-02-23 01:37:02 +00:00

103 lines
3.9 KiB
TypeScript

"use client";
import { IconCheck, IconX, IconLoader2 } from "@tabler/icons-react";
import { useAppStore } from "@/components/state-provider";
import type { PrepareStepInfo } from "@/lib/state/slices/session-runtime/types";
type PrepareProgressProps = {
sessionId: string;
};
function StepIcon({ status }: { status: string }) {
if (status === "completed") {
return <IconCheck className="h-3.5 w-3.5 text-green-500" />;
}
if (status === "failed") {
return <IconX className="h-3.5 w-3.5 text-destructive" />;
}
if (status === "running") {
return <IconLoader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin" />;
}
return <div className="h-3.5 w-3.5 rounded-full border border-muted-foreground/30" />;
}
function StepRow({ step }: { step: PrepareStepInfo }) {
return (
<div className="flex items-start gap-2 text-xs">
<div className="mt-0.5 flex-shrink-0">
<StepIcon status={step.status} />
</div>
<div className="min-w-0 flex-1">
<span className="text-muted-foreground">{step.name || "Preparing..."}</span>
{step.output && (
<pre className="text-muted-foreground/60 mt-0.5 overflow-x-auto whitespace-pre text-xs">
{step.output}
</pre>
)}
{step.error && (
<pre className="text-destructive mt-0.5 overflow-x-auto whitespace-pre text-xs">
{step.error}
</pre>
)}
</div>
</div>
);
}
function useEffectivePrepareStatus(sessionId: string) {
const prepareState = useAppStore((state) => state.prepareProgress.bySessionId[sessionId] ?? null);
const sessionState = useAppStore((state) => state.taskSessions.items[sessionId]?.state);
const profileLabel = useAppStore((state) => {
const session = state.taskSessions.items[sessionId];
if (!session?.agent_profile_id) return null;
const profile = state.agentProfiles.items.find((p) => p.id === session.agent_profile_id);
return profile?.label ?? null;
});
if (!prepareState)
return { visible: false, status: "preparing", prepareState, profileLabel } as const;
if (prepareState.status === "completed")
return { visible: false, status: "completed", prepareState, profileLabel } as const;
// If session reached a terminal state but prepare status is still "preparing",
// treat it as failed (the completed event may not have arrived)
const isSessionTerminal =
sessionState === "FAILED" || sessionState === "COMPLETED" || sessionState === "CANCELLED";
const status =
prepareState.status === "preparing" && isSessionTerminal ? "failed" : prepareState.status;
return { visible: true, status, prepareState, profileLabel } as const;
}
export function PrepareProgress({ sessionId }: PrepareProgressProps) {
const { visible, status, prepareState, profileLabel } = useEffectivePrepareStatus(sessionId);
if (!visible || !prepareState) return null;
const visibleSteps = prepareState.steps.filter(
(step) => step.name.trim() !== "" || Boolean(step.output) || Boolean(step.error),
);
return (
<div className="px-3 py-2 space-y-1.5 border-b border-border/50">
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
{status === "preparing" && <IconLoader2 className="h-3 w-3 animate-spin" />}
{status === "failed" && <IconX className="h-3 w-3 text-destructive" />}
<span>
{status === "preparing" && "Preparing environment..."}
{status === "failed" && (prepareState.errorMessage ?? "Environment preparation failed")}
</span>
{profileLabel && (
<span className="text-muted-foreground/50 ml-auto font-normal">{profileLabel}</span>
)}
</div>
{visibleSteps.length > 0 && (
<div className="space-y-1 pl-1">
{visibleSteps.map((step, i) => (
<StepRow key={i} step={step} />
))}
</div>
)}
</div>
);
}