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; 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 ( ); }; // ── Detail Drawer ──────────────────────────────────────────────────────── type DetailTab = "properties" | "logs"; const DetailTabButton = (props: { label: string; active: boolean; onClick: () => void; }) => ( ); const PropertiesTab = (props: { envelope: ExecutionEnvelope }) => { const { execution, pendingInteraction } = props.envelope; return (
Status
{execution.status.replaceAll("_", " ")}
Duration
{formatDuration(execution) ?? "—"}
Created {formatTimestamp(execution.createdAt)}
Started {formatTimestamp(execution.startedAt)}
{execution.errorText && ( )} {pendingInteraction && (
Pending interaction {pendingInteraction.kind} — {pendingInteraction.purpose}
)}
); }; 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 ( ); } return (
Logs
{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 (
{typeof line === "string" ? line : JSON.stringify(line)}
); })}
); }; const ExecutionDetailDrawer = (props: { executionId: string; onClose: () => void; }) => { const [tab, setTab] = useState("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 ( !open && props.onClose()}> {/* Header */}
{props.executionId}
{(envelope) => ( )}
{/* Tabs */}
setTab("properties")} /> setTab("logs")} />
{/* Content */}
{(envelope) => tab === "properties" ? ( ) : ( ) }
); }; // ── List Page ──────────────────────────────────────────────────────────── type ExecutionHistorySearch = { executionId?: string; }; export function ExecutionHistoryPage() { const executions = useExecutions(); const navigation = useExecutorPluginNavigation(); const searchState = useExecutorPluginSearch(); const [statusFilter, setStatusFilter] = useState("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 (
{/* Header */}

Execution history

Every execution recorded for this workspace, newest first.

{/* Filter bar */}
setSearch((e.target as HTMLInputElement).value)} className="flex-1" />
{/* Column headers */}
_time Raw Data
{/* List */}
{() => filtered.length === 0 ? (
) : (
{filtered.map((execution) => ( setSelectedId( selectedId === execution.id ? null : execution.id, ) } /> ))}
) }
{/* Drawer */} {selectedId && ( setSelectedId(null)} /> )}
); } // ── Detail Page (deep-link) ────────────────────────────────────────────── const ExecutionDetailInline = (props: { envelope: ExecutionEnvelope }) => { const [tab, setTab] = useState("properties"); return (
setTab("properties")} /> setTab("logs")} />
{tab === "properties" ? ( ) : ( )}
); }; export function ExecutionHistoryDetailPage() { const { executionId } = useExecutorPluginRouteParams<{ executionId: string; }>(); const navigation = useExecutorPluginNavigation(); const execution = useExecution(executionId); return (

{executionId}

{(envelope) => ( )}
); }