open-swe/apps/web/src/components/v2/thread-card.tsx
Dylan Boudro d51af805e2
Feat: Thread Status SWR Polling (#370)
* init swr polling branch

* useThreads types improvement

* minor build fix

* major refactor and simplification of polling system

* remove temp guide file

* refactor, remove errorBoundary, simplify schema

* refactor: unify client<>server status logic

* fix threads/page conflicts + functionality

* Update apps/web/src/components/v2/thread-card.tsx

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* CR in progress

* CR fix: add cn to thread-card.tsx

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* CR

* CR

* CR: no threads state, Batch ThreadsStatus for /chat/threads actual logic

* drop req type validation in lg proxy

* optimize thread polling, reduce unnecessary api calls, match implementation spec

* reduce unnecessary api calls, update all threads page

* add thread loading state to threadCard

* utilize threads/search for manager thread info

* improve polling api req efficiency

* rm comments

* post merge format

* fix import

* improve programmer cache logic

* reduce run requests

* remove timeout run requests

* remove run check for completed tasks

* fix: update status dot to support dark mode styles too

* fix session cache and proper typing

* fix: more typing

* code review fixes

* CR

* code review

* fix build

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
2025-07-15 17:51:12 -07:00

211 lines
6.9 KiB
TypeScript

import {
Bug,
CheckCircle,
GitBranch,
GitPullRequest,
Loader2,
AlertCircle,
Pause,
XCircle,
Clock,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Skeleton } from "../ui/skeleton";
import { ThreadMetadata } from "./types";
import { ThreadUIStatus } from "@/lib/schemas/thread-status";
import { cn } from "@/lib/utils";
interface ThreadCardProps {
thread: ThreadMetadata;
status?: ThreadUIStatus;
statusLoading?: boolean;
}
export function ThreadCard({ thread, status, statusLoading }: ThreadCardProps) {
const router = useRouter();
const isStatusLoading = statusLoading && !status;
const displayStatus = status || ("idle" as ThreadUIStatus);
const getStatusColor = (status: ThreadUIStatus) => {
switch (status) {
case "running":
return "dark:bg-blue-950 bg-blue-100 dark:text-blue-400 text-blue-700";
case "completed":
return "dark:bg-green-950 bg-green-100 dark:text-green-400 text-green-700";
case "error":
return "dark:bg-red-950 bg-red-100 dark:text-red-400 text-red-700";
case "paused":
return "dark:bg-yellow-950 bg-yellow-100 dark:text-yellow-400 text-yellow-700";
case "failed":
return "dark:bg-red-950 bg-red-100 dark:text-red-400 text-red-700";
case "pending":
return "dark:bg-yellow-950 bg-yellow-100 dark:text-yellow-400 text-yellow-700";
default:
return "dark:bg-gray-800 bg-gray-200 dark:text-gray-400 text-gray-700";
}
};
const getStatusIcon = (status: ThreadUIStatus) => {
switch (status) {
case "running":
return <Loader2 className="h-4 w-4 animate-spin" />;
case "completed":
return <CheckCircle className="h-4 w-4" />;
case "error":
return <AlertCircle className="h-4 w-4" />;
case "idle":
return <Clock className="h-4 w-4" />;
case "paused":
return <Pause className="h-4 w-4" />;
case "failed":
return <XCircle className="h-4 w-4" />;
default:
return null;
}
};
const getPRStatusColor = (status: string) => {
switch (status) {
case "merged":
return "dark:text-purple-400 text-purple-600";
case "open":
return "dark:text-green-400 text-green-600";
case "draft":
return "dark:text-gray-400 text-gray-600";
case "closed":
return "dark:text-red-400 text-red-600";
default:
return "dark:text-gray-400 text-gray-600";
}
};
return (
<Card
key={thread.id}
className="border-border bg-card hover:bg-muted cursor-pointer px-0 py-3 transition-shadow hover:shadow-lg dark:bg-gray-950"
onClick={() => {
router.push(`/chat/${thread.id}`);
}}
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="text-foreground truncate text-sm font-medium">
{thread.title}
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="text-muted-foreground h-2 w-2" />
<span className="text-muted-foreground truncate text-xs">
{thread.repository}
</span>
</div>
</div>
<Badge
variant="secondary"
className={cn(
"text-xs",
isStatusLoading
? "bg-gray-200 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
: getStatusColor(displayStatus),
)}
>
<div className="flex items-center gap-1">
{isStatusLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
getStatusIcon(displayStatus)
)}
<span className="capitalize">
{isStatusLoading ? "Loading..." : displayStatus}
</span>
</div>
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
{thread.taskCount === 0
? "No tasks"
: `${thread.taskCount} tasks`}
</span>
<span className="text-muted-foreground text-xs">•</span>
<span className="text-muted-foreground text-xs">
{thread.lastActivity}
</span>
</div>
<div className="flex items-center gap-1">
{thread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground h-5 w-5 p-0"
onClick={(e) => {
e.stopPropagation();
window.open(thread.githubIssue!.url, "_blank");
}}
>
<Bug className="h-3 w-3" />
</Button>
)}
{thread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={cn(
"h-5 w-5 p-0 hover:text-gray-300",
getPRStatusColor(thread.pullRequest.status),
)}
onClick={(e) => {
e.stopPropagation();
window.open(thread.pullRequest!.url, "_blank");
}}
>
<GitPullRequest className="h-3 w-3" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
}
export function ThreadCardLoading() {
return (
<Card className="border-border bg-card px-0 py-3 dark:bg-gray-950">
<CardHeader>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="text-foreground truncate text-sm font-medium">
<Skeleton className="h-5 w-48" />
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 w-32" />
</div>
</div>
<Skeleton className="h-6 w-24 rounded-full" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-1" />
<Skeleton className="h-3 w-24" />
</div>
<div className="flex items-center gap-1">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-5 w-5 rounded-full" />
</div>
</div>
</CardContent>
</Card>
);
}