- Rename npm packages from @kb/* to @hai/* and update all workspace references - Rename CLI binary from kb to hai and config directory from .kb to .hai - Update dashboard UI branding, titles, and references from kb to hai - Update all test files, CI workflows, and documentation to reflect new naming - Run comprehensive grep verification to ensure no stale kb references remain
111 lines
3.1 KiB
TypeScript
111 lines
3.1 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import type { AgentLogEntry } from "@kb/core";
|
|
|
|
interface AgentLogViewerProps {
|
|
entries: AgentLogEntry[];
|
|
loading: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders agent log entries in a scrollable, monospace container.
|
|
* Auto-scrolls to the bottom as new entries arrive, but pauses
|
|
* auto-scroll when the user scrolls up (scroll-lock).
|
|
*/
|
|
export function AgentLogViewer({ entries, loading }: AgentLogViewerProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const lastScrollTopRef = useRef(0);
|
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
|
|
// Auto-scroll to bottom when new entries arrive (if scroll-lock is not active)
|
|
useEffect(() => {
|
|
if (autoScroll && containerRef.current) {
|
|
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
}
|
|
}, [entries, autoScroll]);
|
|
|
|
const handleScroll = () => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
const currentScrollTop = el.scrollTop;
|
|
const lastScrollTop = lastScrollTopRef.current;
|
|
// Detect scroll direction: up disables auto-scroll, down re-enables it
|
|
if (currentScrollTop < lastScrollTop) {
|
|
setAutoScroll(false);
|
|
} else if (currentScrollTop > lastScrollTop) {
|
|
setAutoScroll(true);
|
|
}
|
|
lastScrollTopRef.current = currentScrollTop;
|
|
};
|
|
|
|
if (loading && entries.length === 0) {
|
|
return (
|
|
<div className="agent-log-viewer" data-testid="agent-log-viewer">
|
|
<div className="agent-log-loading">Loading agent logs…</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (entries.length === 0) {
|
|
return (
|
|
<div className="agent-log-viewer" data-testid="agent-log-viewer">
|
|
<div className="agent-log-empty">No agent output yet.</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="agent-log-viewer"
|
|
data-testid="agent-log-viewer"
|
|
ref={containerRef}
|
|
onScroll={handleScroll}
|
|
style={{
|
|
fontFamily: "monospace",
|
|
fontSize: "13px",
|
|
lineHeight: "1.5",
|
|
overflowY: "auto",
|
|
maxHeight: "500px",
|
|
padding: "12px",
|
|
background: "var(--bg-secondary, #1a1a2e)",
|
|
borderRadius: "6px",
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-word",
|
|
}}
|
|
>
|
|
{entries.map((entry, i) =>
|
|
entry.type === "tool" ? (
|
|
<div
|
|
key={i}
|
|
className="agent-log-tool"
|
|
style={{
|
|
color: "var(--accent, #7c5cbf)",
|
|
margin: "4px 0",
|
|
padding: "2px 6px",
|
|
borderLeft: "3px solid var(--accent, #7c5cbf)",
|
|
background: "rgba(124, 92, 191, 0.08)",
|
|
}}
|
|
>
|
|
⚡ {entry.text}
|
|
{entry.detail && (
|
|
<span
|
|
className="agent-log-tool-detail"
|
|
style={{
|
|
color: "var(--text-muted, #888)",
|
|
fontSize: "12px",
|
|
marginLeft: "6px",
|
|
}}
|
|
>
|
|
— {entry.detail}
|
|
</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span key={i} className="agent-log-text">
|
|
{entry.text}
|
|
</span>
|
|
),
|
|
)}
|
|
</div>
|
|
);
|
|
}
|