CoDEVX / components /AgentLogPanel.tsx
CodexMacTiger
feat: live package-scoped chat and thinking logs
837e3ac
"use client";
import { useCallback, useEffect, useRef } from "react";
import type { AgentLogEntry } from "@/lib/work-package-types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Download } from "lucide-react";
function levelTone(level: AgentLogEntry["level"]) {
if (level === "success") return "text-emerald-700";
if (level === "warn") return "text-amber-700";
if (level === "error") return "text-rose-700";
return "text-foreground";
}
function formatLogTime(value: string) {
return value.slice(11, 19);
}
export function buildAgentLogExportText(args: {
logs: AgentLogEntry[];
busy: boolean;
}) {
const { logs, busy } = args;
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const header = [
`Agent Log Export — ${timestamp}`,
`Total entries: ${logs.length}`,
`Status: ${busy ? "Running" : "Idle"}`,
"",
];
const body = logs.map(
(log) =>
`[${formatLogTime(log.createdAt)}] ${log.level.toUpperCase()} ${log.title}\n${log.detail}\n`,
);
return [...header, ...body].join("\n");
}
export function AgentLogPanel(props: {
logs: AgentLogEntry[];
busy: boolean;
}) {
const { logs, busy } = props;
const bottomRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ block: "end" });
}, [logs.length]);
const exportLogs = useCallback(() => {
const content = buildAgentLogExportText({ logs, busy });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `agent-log-${timestamp}.txt`;
a.click();
URL.revokeObjectURL(url);
}, [logs, busy]);
return (
<div className="flex h-full min-h-0 flex-col rounded-2xl bg-muted/24 px-1">
<div className="px-3 py-2 md:px-4">
<div className="flex items-center justify-between gap-2">
<div>
<div className="text-sm font-semibold">Agent Log</div>
<div className="text-xs text-muted-foreground">
Background activity, command routing, and board updates.
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={busy ? "secondary" : "outline"} className="text-[11px]">
{busy ? "Running" : "Idle"}
</Badge>
<button
type="button"
onClick={exportLogs}
className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:text-foreground"
title="Export full log as .txt"
>
<Download className="h-3 w-3" />
Export
</button>
</div>
</div>
</div>
<ScrollArea className="flex-1 min-h-0">
<div className="px-3 py-1 font-mono text-[11px] leading-5 md:px-4">
{logs.length ? (
<>
{logs.map((log) => (
<div key={log.id} className="py-1 last:border-b-0">
<div className="flex items-start justify-between gap-2">
<div className={["min-w-0", levelTone(log.level)].join(" ")}>
<div className="font-semibold">{log.title}</div>
<div className="mt-0.5 whitespace-pre-wrap break-words text-muted-foreground/90">
{log.detail}
</div>
</div>
<div
className="shrink-0 text-[10px] text-muted-foreground"
suppressHydrationWarning
>
{formatLogTime(log.createdAt)}
</div>
</div>
</div>
))}
<div ref={bottomRef} />
</>
) : (
<div className="text-muted-foreground">
No agent activity yet.
</div>
)}
</div>
</ScrollArea>
</div>
);
}