import { Link } from "@tanstack/react-router" import { ArrowCounterClockwiseIcon, CaretLeftIcon, CaretRightIcon, CheckCircleIcon, } from "@phosphor-icons/react" import { useState } from "react" import type { AgentSource, AgentStatus, AgentThread } from "@/lib/agents/types" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useResolveAgentThread, useThreadsPage } from "@/lib/agents/queries" import { cn } from "@/lib/utils" const PAGE_SIZE = 25 export interface ThreadsPageFilters { resolved?: boolean viewed?: boolean source?: AgentSource status?: AgentStatus q?: string page: number } type TriState = "any" | "true" | "false" const TRI_OPTIONS: Array<{ value: TriState; label: string }> = [ { value: "any", label: "Any" }, { value: "true", label: "Yes" }, { value: "false", label: "No" }, ] const SOURCE_OPTIONS: Array<{ value: AgentSource | "any"; label: string }> = [ { value: "any", label: "Any source" }, { value: "dashboard", label: "Dashboard" }, { value: "github", label: "GitHub" }, { value: "slack", label: "Slack" }, { value: "linear", label: "Linear" }, { value: "schedule", label: "Schedule" }, ] const STATUS_OPTIONS: Array<{ value: AgentStatus | "any"; label: string }> = [ { value: "any", label: "Any status" }, { value: "running", label: "Running" }, { value: "finished", label: "Finished" }, { value: "error", label: "Error" }, { value: "idle", label: "Idle" }, ] function boolToTri(value?: boolean): TriState { if (value === true) return "true" if (value === false) return "false" return "any" } function triToBool(value: TriState): boolean | undefined { if (value === "true") return true if (value === "false") return false return undefined } export function AgentsThreadsPage({ filters, onFiltersChange, }: { filters: ThreadsPageFilters onFiltersChange: (next: ThreadsPageFilters) => void }) { const [search, setSearch] = useState(filters.q ?? "") const offset = (filters.page - 1) * PAGE_SIZE const query = useThreadsPage({ limit: PAGE_SIZE, offset, resolved: filters.resolved, viewed: filters.viewed, source: filters.source, status: filters.status, q: filters.q, }) const data = query.data const items = data?.items ?? [] const hasMore = data?.hasMore ?? false const exactTotal = data?.total const end = offset + items.length const update = (patch: Partial) => { onFiltersChange({ ...filters, ...patch, page: patch.page ?? 1 }) } const onSearchSubmit = (e: React.FormEvent) => { e.preventDefault() update({ q: search.trim() || undefined }) } return (

Threads

Search and filter every thread, including resolved ones.

setSearch(e.target.value)} placeholder="Search by title..." className="h-8" />
update({ resolved: triToBool(value) })} /> update({ viewed: triToBool(value) })} /> update({ source: value === "any" ? undefined : (value as AgentSource), }) } /> update({ status: value === "any" ? undefined : (value as AgentStatus), }) } />
{query.isLoading ? (

Loading...

) : items.length === 0 ? (

No threads match these filters.

) : ( items.map((thread) => ( )) )}
{(items.length > 0 || filters.page > 1) && (
{items.length > 0 ? `${offset + 1}–${end}` : "No results"} {exactTotal != null ? ` of ${exactTotal}` : hasMore ? "+" : ""}
Page {filters.page}
)}
) } function TriFilter({ label, value, onChange, }: { label: string value: TriState onChange: (value: TriState) => void }) { return (
{label}
{TRI_OPTIONS.map((option) => ( ))}
) } function SelectFilter({ value, options, onChange, }: { value: string options: Array<{ value: string; label: string }> onChange: (value: string) => void }) { return ( ) } function ThreadListItem({ thread }: { thread: AgentThread }) { const resolveThread = useResolveAgentThread() const isResolved = thread.resolved === true const onToggleResolved = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() if (resolveThread.isPending) return resolveThread.mutate({ threadId: thread.id, resolved: !isResolved }) } return (

{thread.title}

{thread.repoFullName || "no repo"} · {thread.status} {isResolved ? " · resolved" : ""}

) }