"use client"; import { useEffect, useState, useCallback } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { SessionMessage, SessionMessagePart, CronRunSessionResponse } from "../../types/cron"; /* ─── Main component ─── */ export function CronRunChat({ sessionId }: { sessionId: string }) { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchSession = useCallback(async () => { try { const res = await fetch(`/api/cron/runs/${encodeURIComponent(sessionId)}`); if (!res.ok) { setError(res.status === 404 ? "Session transcript not found" : "Failed to load session"); return; } const data: CronRunSessionResponse = await res.json(); setMessages(data.messages ?? []); } catch { setError("Failed to load session"); } finally { setLoading(false); } }, [sessionId]); useEffect(() => { void fetchSession(); }, [fetchSession]); if (loading) { return (
Loading session transcript...
); } if (error) { return (
{error}
); } if (messages.length === 0) { return (
Empty session transcript.
); } return (
Session Transcript
{messages.map((msg) => ( ))}
); } /* ─── Transcript search fallback (no sessionId) ─── */ export function CronRunTranscriptSearch({ jobId, runAtMs, summary, fallback, }: { jobId: string; runAtMs?: number; summary?: string; fallback?: React.ReactNode; }) { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [notFound, setNotFound] = useState(false); const fetchTranscript = useCallback(async () => { if (!runAtMs || !summary) { setLoading(false); setNotFound(true); return; } try { const params = new URLSearchParams({ jobId, runAtMs: String(runAtMs), summary, }); const res = await fetch(`/api/cron/runs/search-transcript?${params}`); if (!res.ok) { setNotFound(true); return; } const data = await res.json() as { messages?: SessionMessage[] }; if (data.messages && data.messages.length > 0) { setMessages(data.messages); } else { setNotFound(true); } } catch { setNotFound(true); } finally { setLoading(false); } }, [jobId, runAtMs, summary]); useEffect(() => { void fetchTranscript(); }, [fetchTranscript]); if (loading) { return (
Searching for transcript...
); } if (notFound || messages.length === 0) { return <>{fallback}; } return (
Session Transcript
{messages.map((msg) => ( ))}
); } /* ─── Message rendering ─── */ function CronChatMessage({ message }: { message: SessionMessage }) { const isUser = message.role === "user"; const isSystem = message.role === "system"; // Group parts into segments const segments = groupPartsIntoSegments(message.parts); if (isSystem) { const textContent = message.parts .filter((p): p is { type: "text"; text: string } => p.type === "text") .map((p) => p.text) .join("\n"); return (
system: {textContent.slice(0, 500)}
); } if (isUser) { const textContent = message.parts .filter((p): p is { type: "text"; text: string } => p.type === "text") .map((p) => p.text) .join("\n"); return (

{textContent}

); } // Assistant message return (
{segments.map((segment, idx) => { if (segment.type === "text") { return (
{segment.text}
); } if (segment.type === "thinking") { return ; } if (segment.type === "tool-group") { return ; } return null; })}
); } /* ─── Part grouping ─── */ type ChatSegment = | { type: "text"; text: string } | { type: "thinking"; thinking: string } | { type: "tool-group"; tools: Array }; function groupPartsIntoSegments(parts: SessionMessagePart[]): ChatSegment[] { const segments: ChatSegment[] = []; let toolBuffer: Array = []; const flushTools = () => { if (toolBuffer.length > 0) { segments.push({ type: "tool-group", tools: [...toolBuffer] }); toolBuffer = []; } }; for (const part of parts) { if (part.type === "text") { flushTools(); segments.push({ type: "text", text: part.text }); } else if (part.type === "thinking") { flushTools(); segments.push({ type: "thinking", thinking: part.thinking }); } else if (part.type === "tool-call") { toolBuffer.push(part as SessionMessagePart & { type: "tool-call" }); } } flushTools(); return segments; } /* ─── Thinking block (always expanded for historical runs) ─── */ function ThinkingBlock({ text }: { text: string }) { const [expanded, setExpanded] = useState(true); const isLong = text.length > 600; return (
{expanded && (
{text}
)}
); } /* ─── Tool group ─── */ function ToolGroup({ tools }: { tools: Array }) { return (
{/* Timeline connector */}
{tools.map((tool) => ( ))}
); } /* ─── Tool call step ─── */ function ToolCallStep({ tool }: { tool: SessionMessagePart & { type: "tool-call" } }) { const [showOutput, setShowOutput] = useState(false); const label = buildToolLabel(tool.toolName, tool.args); return (
{label}
{tool.output && (
{showOutput && (
                {tool.output.length > 3000 ? tool.output.slice(0, 3000) + "\n..." : tool.output}
              
)}
)}
); } /* ─── Tool label builder ─── */ function buildToolLabel(toolName: string, args?: unknown): string { const a = args as Record | undefined; const strVal = (key: string) => { const v = a?.[key]; return typeof v === "string" && v.length > 0 ? v : null; }; const n = toolName.toLowerCase().replace(/[_-]/g, ""); if (["websearch", "search", "googlesearch"].some((k) => n.includes(k))) { const q = strVal("query") ?? strVal("search_query") ?? strVal("q"); return q ? `Searching: ${q.slice(0, 80)}` : "Searching..."; } if (["fetchurl", "fetch", "webfetch"].some((k) => n.includes(k))) { const u = strVal("url") ?? strVal("path"); return u ? `Fetching: ${u.slice(0, 60)}` : "Fetching page"; } if (["read", "readfile", "getfile"].some((k) => n.includes(k))) { const p = strVal("path") ?? strVal("file"); return p ? `Reading: ${p.split("/").pop()}` : "Reading file"; } if (["bash", "shell", "execute", "exec", "terminal"].some((k) => n.includes(k))) { const cmd = strVal("command") ?? strVal("cmd"); return cmd ? `Running: ${cmd.slice(0, 60)}` : "Running command"; } if (["write", "create", "edit", "str_replace", "save"].some((k) => n.includes(k))) { const p = strVal("path") ?? strVal("file"); return p ? `Editing: ${p.split("/").pop()}` : "Editing file"; } return toolName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } /* ─── Tool icon ─── */ function ToolIcon({ toolName }: { toolName: string }) { const color = "var(--color-text-muted)"; const n = toolName.toLowerCase().replace(/[_-]/g, ""); if (["search", "websearch"].some((k) => n.includes(k))) { return ( ); } if (["bash", "shell", "exec", "terminal"].some((k) => n.includes(k))) { return ( ); } if (["write", "edit", "create", "save"].some((k) => n.includes(k))) { return ( ); } // Default: file/read icon return ( ); }