import { useState, useMemo, useCallback } from "react";import type { Execution, ExecutionEnvelope,} from "@executor/react";import { useExecution, useExecutions,} from "@executor/react";import { Button, cn, Dialog, DialogOverlay, DialogPopup, DialogPortal, DocumentPanel, EmptyState, IconClose, Input, LoadableBlock, Select, useExecutorPluginNavigation, useExecutorPluginRouteParams, useExecutorPluginSearch,} from "@executor/react/plugins"; // ── Helpers ────────────────────────────────────────────────────────────── type ExecutionStatus = Execution["status"]; const formatTimestamp = (value: number | null): string => { if (value === null) return "—"; const d = new Date(value); const month = d.toLocaleString(undefined, { month: "short" }); const day = d.getDate(); const h = d.getHours().toString().padStart(2, "0"); const m = d.getMinutes().toString().padStart(2, "0"); const s = d.getSeconds().toString().padStart(2, "0"); return `${month} ${day}, ${h}:${m}:${s}`;}; const formatDuration = (execution: Execution): string | null => { if (execution.startedAt === null || execution.completedAt === null) return null; const ms = Math.max(0, execution.completedAt - execution.startedAt); if (ms < 1_000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1_000).toFixed(1)}s`; return `${(ms / 60_000).toFixed(1)}m`;}; const truncateCode = (code: string, max = 80): string => code.trim().replace(/\s+/g, " ").slice(0, max); const formatJsonDocument = (value: string | null): string | null => { if (value === null) return null; try { return JSON.stringify(JSON.parse(value), null, 2); } catch { return value; }}; const STATUS_STYLES = { completed: { dot: "bg-primary", text: "text-primary" }, failed: { dot: "bg-destructive", text: "text-destructive" }, running: { dot: "bg-blue-400 animate-pulse", text: "text-blue-400" }, waiting_for_interaction: { dot: "bg-amber-400 animate-pulse", text: "text-amber-400" }, pending: { dot: "bg-muted", text: "text-muted-foreground" }, cancelled: { dot: "bg-muted", text: "text-muted-foreground" },} as const satisfies Record<ExecutionStatus, { dot: string; text: string }>; const statusDot = (status: ExecutionStatus): string => STATUS_STYLES[status].dot;const statusText = (status: ExecutionStatus): string => STATUS_STYLES[status].text; const STATUS_OPTIONS: Array<{ value: "all" | ExecutionStatus; label: string }> = [ { value: "all", label: "All" }, { value: "completed", label: "Completed" }, { value: "failed", label: "Failed" }, { value: "running", label: "Running" }, { value: "waiting_for_interaction", label: "Waiting" }, { value: "pending", label: "Pending" }, { value: "cancelled", label: "Cancelled" },]; const formatDurationMs = (execution: Execution): string | null => { if (execution.startedAt === null || execution.completedAt === null) return null; const ms = Math.max(0, execution.completedAt - execution.startedAt); return ms.toLocaleString();}; // ── Execution Row ──────────────────────────────────────────────────────── const ExecutionRow = (props: { execution: Execution; isSelected: boolean; onSelect: () => void;}) => { const { execution, isSelected } = props; const durationMs = formatDurationMs(execution); return ( <button type="button" onClick={props.onSelect} className={cn( "group flex w-full items-start gap-3 border-b border-border/50 px-4 py-2 text-left font-mono text-xs transition-colors hover:bg-white/[0.04]", isSelected && "bg-white/[0.06]", )} > <span className={cn( "mt-1.5 size-2 shrink-0 rounded-full", statusDot(execution.status), )} /> <span className="w-[120px] shrink-0 tabular-nums text-muted-foreground"> {formatTimestamp(execution.createdAt)} </span> <span className="inline-flex w-[130px] shrink-0"> <span className="text-muted-foreground/60">status:</span> <span className={statusText(execution.status)}>{execution.status.replaceAll("_", " ")}</span> </span> <span className="inline-flex w-[150px] shrink-0"> <span className="text-muted-foreground/60">duration_ms:</span>{" "} <span className={durationMs && Number(durationMs.replace(/,/g, "")) > 5000 ? "text-destructive" : "text-primary"}>{durationMs ?? "—"}</span> </span> <span className="min-w-0 flex-1 truncate"> <span className="text-muted-foreground/60">code:</span>{" "} <span className="text-muted-foreground">"{truncateCode(execution.code, 120)}…"</span> </span> </button> );}; // ── Detail Drawer ──────────────────────────────────────────────────────── type DetailTab = "properties" | "logs"; const DetailTabButton = (props: { label: string; active: boolean; onClick: () => void;}) => ( <button type="button" onClick={props.onClick} className={cn( "px-3 py-1.5 text-sm font-medium transition-colors", props.active ? "border-b-2 border-primary text-foreground" : "text-muted-foreground hover:text-foreground", )} > {props.label} </button>); const PropertiesTab = (props: { envelope: ExecutionEnvelope }) => { const { execution, pendingInteraction } = props.envelope; return ( <div className="space-y-4"> <div className="grid grid-cols-2 gap-3"> <div className="rounded-lg border border-border/50 px-3 py-2"> <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Status </div> <div className="mt-1 flex items-center gap-2"> <span className={cn("size-2 rounded-full", statusDot(execution.status))} /> <span className="text-sm">{execution.status.replaceAll("_", " ")}</span> </div> </div> <div className="rounded-lg border border-border/50 px-3 py-2"> <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Duration </div> <div className="mt-1 text-sm">{formatDuration(execution) ?? "—"}</div> </div> </div> <div className="grid grid-cols-2 gap-3 text-xs text-muted-foreground"> <div> <span className="text-muted-foreground/60">Created </span> {formatTimestamp(execution.createdAt)} </div> <div> <span className="text-muted-foreground/60">Started </span> {formatTimestamp(execution.startedAt)} </div> </div> <DocumentPanel title="Code" body={execution.code} lang="ts" empty="No code captured." /> <DocumentPanel title="Result" body={formatJsonDocument(execution.resultJson)} lang="json" empty="No result recorded." /> {execution.errorText && ( <DocumentPanel title="Error" body={execution.errorText} lang="text" empty="No error recorded." /> )} {pendingInteraction && ( <div className="space-y-3"> <div className="text-sm font-medium text-foreground"> Pending interaction <span className="ml-2 text-xs text-muted-foreground"> {pendingInteraction.kind} — {pendingInteraction.purpose} </span> </div> <div className="grid gap-3 xl:grid-cols-2"> <DocumentPanel title="Request" body={formatJsonDocument(pendingInteraction.payloadJson)} lang="json" empty="No interaction request recorded." /> <DocumentPanel title="Response" body={formatJsonDocument(pendingInteraction.responseJson)} lang="json" empty="No interaction response recorded." /> </div> </div> )} </div> );}; const LogsTab = (props: { logsJson: string | null }) => { const parsed = useMemo(() => { if (!props.logsJson) return null; try { const arr = JSON.parse(props.logsJson); if (!Array.isArray(arr)) return null; return arr as string[]; } catch { return null; } }, [props.logsJson]); if (!parsed) { return ( <DocumentPanel title="Logs" body={formatJsonDocument(props.logsJson)} lang="json" empty="No logs recorded." /> ); } return ( <div className="rounded-lg border border-border/50 bg-card/40 p-3"> <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Logs </div> <div className="space-y-0.5 font-mono text-xs"> {parsed.map((line, i) => { const isError = typeof line === "string" && /\[error]/i.test(line); const isWarn = typeof line === "string" && /\[warn]/i.test(line); const lineKey = typeof line === "string" ? `line-${i}-${line.slice(0, 50).replace(/\s+/g, "-")}` : `line-${i}-${JSON.stringify(line).slice(0, 50)}`; return ( <div key={lineKey} className={cn( "whitespace-pre-wrap break-all", isError && "text-red-400", isWarn && "text-amber-400", !isError && !isWarn && "text-foreground/80", )} > {typeof line === "string" ? line : JSON.stringify(line)} </div> ); })} </div> </div> );}; const ExecutionDetailDrawer = (props: { executionId: string; onClose: () => void;}) => { const [tab, setTab] = useState<DetailTab>("properties"); const execution = useExecution(props.executionId); const [copied, setCopied] = useState(false); const handleCopy = useCallback( (envelope: ExecutionEnvelope) => { const tryParse = (v: string | null) => { if (v === null) return null; try { return JSON.parse(v); } catch { return v; } }; const cleaned = { ...envelope, execution: { ...envelope.execution, resultJson: tryParse(envelope.execution.resultJson), logsJson: tryParse(envelope.execution.logsJson), errorText: envelope.execution.errorText, }, }; void navigator.clipboard .writeText(JSON.stringify(cleaned, null, 2)) .then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [], ); return ( <Dialog open onOpenChange={(open) => !open && props.onClose()}> <DialogPortal> <DialogOverlay /> <DialogPopup className="fixed inset-y-0 right-0 z-50 flex w-full max-w-3xl flex-col bg-popover text-popover-foreground ring-1 ring-foreground/10 outline-none duration-150 data-open:animate-in data-open:slide-in-from-right data-closed:animate-out data-closed:slide-out-to-right" > {/* Header */} <div className="flex items-center justify-between border-b border-border/50 px-5 py-3"> <div className="min-w-0"> <div className="truncate text-sm font-medium text-foreground"> {props.executionId} </div> </div> <div className="flex items-center gap-2"> <LoadableBlock loadable={execution} loading=""> {(envelope) => ( <Button variant="ghost" size="sm" onClick={() => handleCopy(envelope)} className="text-xs text-muted-foreground" > {copied ? "Copied" : "Copy JSON"} </Button> )} </LoadableBlock> <Button variant="ghost" size="icon-sm" onClick={props.onClose} > <IconClose className="size-3.5" /> </Button> </div> </div> {/* Tabs */} <div className="flex gap-1 border-b border-border/50 px-5"> <DetailTabButton label="Properties" active={tab === "properties"} onClick={() => setTab("properties")} /> <DetailTabButton label="Logs" active={tab === "logs"} onClick={() => setTab("logs")} /> </div> {/* Content */} <div className="flex-1 overflow-y-auto px-5 py-4"> <LoadableBlock loadable={execution} loading="Loading execution..."> {(envelope) => tab === "properties" ? ( <PropertiesTab envelope={envelope} /> ) : ( <LogsTab logsJson={envelope.execution.logsJson} /> ) } </LoadableBlock> </div> </DialogPopup> </DialogPortal> </Dialog> );}; // ── List Page ──────────────────────────────────────────────────────────── type ExecutionHistorySearch = { executionId?: string;}; export function ExecutionHistoryPage() { const executions = useExecutions(); const navigation = useExecutorPluginNavigation(); const searchState = useExecutorPluginSearch<ExecutionHistorySearch>(); const [statusFilter, setStatusFilter] = useState<string>("all"); const [search, setSearch] = useState(""); const selectedId = typeof searchState.executionId === "string" ? searchState.executionId : null; const setSelectedId = useCallback( (executionId: string | null) => { if (executionId === null) { const { executionId: _, ...nextSearch } = searchState; void navigation.updateSearch(nextSearch); return; } void navigation.updateSearch({ ...searchState, executionId, }); }, [navigation, searchState], ); const filtered = useMemo(() => { if (executions.status !== "ready") return []; let items = [...executions.data]; if (statusFilter !== "all") { items = items.filter((e) => e.status === statusFilter); } if (search.trim()) { const q = search.toLowerCase(); items = items.filter( (e) => e.code.toLowerCase().includes(q) || (e.errorText && e.errorText.toLowerCase().includes(q)), ); } return items; }, [executions, statusFilter, search]); return ( <div className="flex h-full flex-col"> {/* Header */} <div className="shrink-0 border-b border-border/50 px-6 py-5"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Execution history </h1> <p className="mt-1 text-sm text-muted-foreground"> Every execution recorded for this workspace, newest first. </p> {/* Filter bar */} <div className="mt-4 flex items-center gap-3"> <Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="w-[140px]" > {STATUS_OPTIONS.map((opt) => ( <option key={opt.value} value={opt.value}> {opt.label} </option> ))} </Select> <Input type="text" placeholder="Search code or errors..." value={search} onChange={(e) => setSearch((e.target as HTMLInputElement).value)} className="flex-1" /> </div> </div> {/* Column headers */} <div className="flex items-center gap-3 border-b border-border/50 px-4 py-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> <span className="size-2 shrink-0" /> <span className="w-[120px] shrink-0">_time</span> <span>Raw Data</span> </div> {/* List */} <div className="flex-1 overflow-y-auto"> <LoadableBlock loadable={executions} loading="Loading executions..."> {() => filtered.length === 0 ? ( <div className="py-20"> <EmptyState title={ statusFilter !== "all" || search.trim() ? "No matching executions" : "No executions yet"Lines 1–500 of 604. Load more
import { useState, useMemo, useCallback } from "react"; import type { Execution, ExecutionEnvelope, } from "@executor/react"; import { useExecution, useExecutions, } from "@executor/react"; import { Button, cn, Dialog, DialogOverlay, DialogPopup, DialogPortal, DocumentPanel, EmptyState, IconClose, Input, LoadableBlock, Select, useExecutorPluginNavigation, useExecutorPluginRouteParams, useExecutorPluginSearch, } from "@executor/react/plugins"; // ── Helpers ────────────────────────────────────────────────────────────── type ExecutionStatus = Execution["status"]; const formatTimestamp = (value: number | null): string => { if (value === null) return "—"; const d = new Date(value); const month = d.toLocaleString(undefined, { month: "short" }); const day = d.getDate(); const h = d.getHours().toString().padStart(2, "0"); const m = d.getMinutes().toString().padStart(2, "0"); const s = d.getSeconds().toString().padStart(2, "0"); return `${month} ${day}, ${h}:${m}:${s}`; }; const formatDuration = (execution: Execution): string | null => { if (execution.startedAt === null || execution.completedAt === null) return null; const ms = Math.max(0, execution.completedAt - execution.startedAt); if (ms < 1_000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1_000).toFixed(1)}s`; return `${(ms / 60_000).toFixed(1)}m`; }; const truncateCode = (code: string, max = 80): string => code.trim().replace(/\s+/g, " ").slice(0, max); const formatJsonDocument = (value: string | null): string | null => { if (value === null) return null; try { return JSON.stringify(JSON.parse(value), null, 2); } catch { return value; } }; const STATUS_STYLES = { completed: { dot: "bg-primary", text: "text-primary" }, failed: { dot: "bg-destructive", text: "text-destructive" }, running: { dot: "bg-blue-400 animate-pulse", text: "text-blue-400" }, waiting_for_interaction: { dot: "bg-amber-400 animate-pulse", text: "text-amber-400" }, pending: { dot: "bg-muted", text: "text-muted-foreground" }, cancelled: { dot: "bg-muted", text: "text-muted-foreground" }, } as const satisfies Record<ExecutionStatus, { dot: string; text: string }>; const statusDot = (status: ExecutionStatus): string => STATUS_STYLES[status].dot; const statusText = (status: ExecutionStatus): string => STATUS_STYLES[status].text; const STATUS_OPTIONS: Array<{ value: "all" | ExecutionStatus; label: string }> = [ { value: "all", label: "All" }, { value: "completed", label: "Completed" }, { value: "failed", label: "Failed" }, { value: "running", label: "Running" }, { value: "waiting_for_interaction", label: "Waiting" }, { value: "pending", label: "Pending" }, { value: "cancelled", label: "Cancelled" }, ]; const formatDurationMs = (execution: Execution): string | null => { if (execution.startedAt === null || execution.completedAt === null) return null; const ms = Math.max(0, execution.completedAt - execution.startedAt); return ms.toLocaleString(); }; // ── Execution Row ──────────────────────────────────────────────────────── const ExecutionRow = (props: { execution: Execution; isSelected: boolean; onSelect: () => void; }) => { const { execution, isSelected } = props; const durationMs = formatDurationMs(execution); return ( <button type="button" onClick={props.onSelect} className={cn( "group flex w-full items-start gap-3 border-b border-border/50 px-4 py-2 text-left font-mono text-xs transition-colors hover:bg-white/[0.04]", isSelected && "bg-white/[0.06]", )} > <span className={cn( "mt-1.5 size-2 shrink-0 rounded-full", statusDot(execution.status), )} /> <span className="w-[120px] shrink-0 tabular-nums text-muted-foreground"> {formatTimestamp(execution.createdAt)} </span> <span className="inline-flex w-[130px] shrink-0"> <span className="text-muted-foreground/60">status:</span> <span className={statusText(execution.status)}>{execution.status.replaceAll("_", " ")}</span> </span> <span className="inline-flex w-[150px] shrink-0"> <span className="text-muted-foreground/60">duration_ms:</span>{" "} <span className={durationMs && Number(durationMs.replace(/,/g, "")) > 5000 ? "text-destructive" : "text-primary"}>{durationMs ?? "—"}</span> </span> <span className="min-w-0 flex-1 truncate"> <span className="text-muted-foreground/60">code:</span>{" "} <span className="text-muted-foreground">"{truncateCode(execution.code, 120)}…"</span> </span> </button> ); }; // ── Detail Drawer ──────────────────────────────────────────────────────── type DetailTab = "properties" | "logs"; const DetailTabButton = (props: { label: string; active: boolean; onClick: () => void; }) => ( <button type="button" onClick={props.onClick} className={cn( "px-3 py-1.5 text-sm font-medium transition-colors", props.active ? "border-b-2 border-primary text-foreground" : "text-muted-foreground hover:text-foreground", )} > {props.label} </button> ); const PropertiesTab = (props: { envelope: ExecutionEnvelope }) => { const { execution, pendingInteraction } = props.envelope; return ( <div className="space-y-4"> <div className="grid grid-cols-2 gap-3"> <div className="rounded-lg border border-border/50 px-3 py-2"> <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Status </div> <div className="mt-1 flex items-center gap-2"> <span className={cn("size-2 rounded-full", statusDot(execution.status))} /> <span className="text-sm">{execution.status.replaceAll("_", " ")}</span> </div> </div> <div className="rounded-lg border border-border/50 px-3 py-2"> <div className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Duration </div> <div className="mt-1 text-sm">{formatDuration(execution) ?? "—"}</div> </div> </div> <div className="grid grid-cols-2 gap-3 text-xs text-muted-foreground"> <div> <span className="text-muted-foreground/60">Created </span> {formatTimestamp(execution.createdAt)} </div> <div> <span className="text-muted-foreground/60">Started </span> {formatTimestamp(execution.startedAt)} </div> </div> <DocumentPanel title="Code" body={execution.code} lang="ts" empty="No code captured." /> <DocumentPanel title="Result" body={formatJsonDocument(execution.resultJson)} lang="json" empty="No result recorded." /> {execution.errorText && ( <DocumentPanel title="Error" body={execution.errorText} lang="text" empty="No error recorded." /> )} {pendingInteraction && ( <div className="space-y-3"> <div className="text-sm font-medium text-foreground"> Pending interaction <span className="ml-2 text-xs text-muted-foreground"> {pendingInteraction.kind} — {pendingInteraction.purpose} </span> </div> <div className="grid gap-3 xl:grid-cols-2"> <DocumentPanel title="Request" body={formatJsonDocument(pendingInteraction.payloadJson)} lang="json" empty="No interaction request recorded." /> <DocumentPanel title="Response" body={formatJsonDocument(pendingInteraction.responseJson)} lang="json" empty="No interaction response recorded." /> </div> </div> )} </div> ); }; const LogsTab = (props: { logsJson: string | null }) => { const parsed = useMemo(() => { if (!props.logsJson) return null; try { const arr = JSON.parse(props.logsJson); if (!Array.isArray(arr)) return null; return arr as string[]; } catch { return null; } }, [props.logsJson]); if (!parsed) { return ( <DocumentPanel title="Logs" body={formatJsonDocument(props.logsJson)} lang="json" empty="No logs recorded." /> ); } return ( <div className="rounded-lg border border-border/50 bg-card/40 p-3"> <div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> Logs </div> <div className="space-y-0.5 font-mono text-xs"> {parsed.map((line, i) => { const isError = typeof line === "string" && /\[error]/i.test(line); const isWarn = typeof line === "string" && /\[warn]/i.test(line); const lineKey = typeof line === "string" ? `line-${i}-${line.slice(0, 50).replace(/\s+/g, "-")}` : `line-${i}-${JSON.stringify(line).slice(0, 50)}`; return ( <div key={lineKey} className={cn( "whitespace-pre-wrap break-all", isError && "text-red-400", isWarn && "text-amber-400", !isError && !isWarn && "text-foreground/80", )} > {typeof line === "string" ? line : JSON.stringify(line)} </div> ); })} </div> </div> ); }; const ExecutionDetailDrawer = (props: { executionId: string; onClose: () => void; }) => { const [tab, setTab] = useState<DetailTab>("properties"); const execution = useExecution(props.executionId); const [copied, setCopied] = useState(false); const handleCopy = useCallback( (envelope: ExecutionEnvelope) => { const tryParse = (v: string | null) => { if (v === null) return null; try { return JSON.parse(v); } catch { return v; } }; const cleaned = { ...envelope, execution: { ...envelope.execution, resultJson: tryParse(envelope.execution.resultJson), logsJson: tryParse(envelope.execution.logsJson), errorText: envelope.execution.errorText, }, }; void navigator.clipboard .writeText(JSON.stringify(cleaned, null, 2)) .then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [], ); return ( <Dialog open onOpenChange={(open) => !open && props.onClose()}> <DialogPortal> <DialogOverlay /> <DialogPopup className="fixed inset-y-0 right-0 z-50 flex w-full max-w-3xl flex-col bg-popover text-popover-foreground ring-1 ring-foreground/10 outline-none duration-150 data-open:animate-in data-open:slide-in-from-right data-closed:animate-out data-closed:slide-out-to-right" > {/* Header */} <div className="flex items-center justify-between border-b border-border/50 px-5 py-3"> <div className="min-w-0"> <div className="truncate text-sm font-medium text-foreground"> {props.executionId} </div> </div> <div className="flex items-center gap-2"> <LoadableBlock loadable={execution} loading=""> {(envelope) => ( <Button variant="ghost" size="sm" onClick={() => handleCopy(envelope)} className="text-xs text-muted-foreground" > {copied ? "Copied" : "Copy JSON"} </Button> )} </LoadableBlock> <Button variant="ghost" size="icon-sm" onClick={props.onClose} > <IconClose className="size-3.5" /> </Button> </div> </div> {/* Tabs */} <div className="flex gap-1 border-b border-border/50 px-5"> <DetailTabButton label="Properties" active={tab === "properties"} onClick={() => setTab("properties")} /> <DetailTabButton label="Logs" active={tab === "logs"} onClick={() => setTab("logs")} /> </div> {/* Content */} <div className="flex-1 overflow-y-auto px-5 py-4"> <LoadableBlock loadable={execution} loading="Loading execution..."> {(envelope) => tab === "properties" ? ( <PropertiesTab envelope={envelope} /> ) : ( <LogsTab logsJson={envelope.execution.logsJson} /> ) } </LoadableBlock> </div> </DialogPopup> </DialogPortal> </Dialog> ); }; // ── List Page ──────────────────────────────────────────────────────────── type ExecutionHistorySearch = { executionId?: string; }; export function ExecutionHistoryPage() { const executions = useExecutions(); const navigation = useExecutorPluginNavigation(); const searchState = useExecutorPluginSearch<ExecutionHistorySearch>(); const [statusFilter, setStatusFilter] = useState<string>("all"); const [search, setSearch] = useState(""); const selectedId = typeof searchState.executionId === "string" ? searchState.executionId : null; const setSelectedId = useCallback( (executionId: string | null) => { if (executionId === null) { const { executionId: _, ...nextSearch } = searchState; void navigation.updateSearch(nextSearch); return; } void navigation.updateSearch({ ...searchState, executionId, }); }, [navigation, searchState], ); const filtered = useMemo(() => { if (executions.status !== "ready") return []; let items = [...executions.data]; if (statusFilter !== "all") { items = items.filter((e) => e.status === statusFilter); } if (search.trim()) { const q = search.toLowerCase(); items = items.filter( (e) => e.code.toLowerCase().includes(q) || (e.errorText && e.errorText.toLowerCase().includes(q)), ); } return items; }, [executions, statusFilter, search]); return ( <div className="flex h-full flex-col"> {/* Header */} <div className="shrink-0 border-b border-border/50 px-6 py-5"> <h1 className="font-display text-2xl tracking-tight text-foreground"> Execution history </h1> <p className="mt-1 text-sm text-muted-foreground"> Every execution recorded for this workspace, newest first. </p> {/* Filter bar */} <div className="mt-4 flex items-center gap-3"> <Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="w-[140px]" > {STATUS_OPTIONS.map((opt) => ( <option key={opt.value} value={opt.value}> {opt.label} </option> ))} </Select> <Input type="text" placeholder="Search code or errors..." value={search} onChange={(e) => setSearch((e.target as HTMLInputElement).value)} className="flex-1" /> </div> </div> {/* Column headers */} <div className="flex items-center gap-3 border-b border-border/50 px-4 py-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> <span className="size-2 shrink-0" /> <span className="w-[120px] shrink-0">_time</span> <span>Raw Data</span> </div> {/* List */} <div className="flex-1 overflow-y-auto"> <LoadableBlock loadable={executions} loading="Loading executions..."> {() => filtered.length === 0 ? ( <div className="py-20"> <EmptyState title={ statusFilter !== "all" || search.trim() ? "No matching executions" : "No executions yet"