feat(KB-091): add auto-scroll to agent log viewer
- Add useRef and useEffect hooks to track container scroll position - Auto-scroll to top when new entries arrive while user is near top (within 50px) - Preserve model info header display with executor/validator models - Add tests for auto-scroll behavior (scrolls when at top, preserves position when scrolled) - All 18 AgentLogViewer tests pass
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
interface ModelInfo {
|
||||
provider?: string;
|
||||
@@ -16,8 +17,33 @@ interface AgentLogViewerProps {
|
||||
/**
|
||||
* Renders agent log entries in a scrollable, monospace container.
|
||||
* Displays entries in reverse chronological order (newest first).
|
||||
* Auto-scrolls to keep latest entries visible when streaming.
|
||||
*/
|
||||
export function AgentLogViewer({ entries, loading, executorModel, validatorModel }: AgentLogViewerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const previousEntryCountRef = useRef<number>(0);
|
||||
|
||||
// Auto-scroll to top when new entries arrive (since newest are first)
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const newEntryCount = entries.length;
|
||||
const previousCount = previousEntryCountRef.current;
|
||||
|
||||
// Only scroll if new entries were added and user is near the top
|
||||
if (newEntryCount > previousCount) {
|
||||
// Check if user is already near the top (within 50px)
|
||||
const isNearTop = container.scrollTop <= 50;
|
||||
|
||||
if (isNearTop) {
|
||||
container.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
previousEntryCountRef.current = newEntryCount;
|
||||
}, [entries]);
|
||||
|
||||
if (loading && entries.length === 0) {
|
||||
return (
|
||||
<div className="agent-log-viewer" data-testid="agent-log-viewer">
|
||||
@@ -42,6 +68,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="agent-log-viewer"
|
||||
data-testid="agent-log-viewer"
|
||||
style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { AgentLogViewer } from "../AgentLogViewer";
|
||||
import type { AgentLogEntry } from "@kb/core";
|
||||
@@ -330,4 +330,34 @@ describe("AgentLogViewer", () => {
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-scroll behavior", () => {
|
||||
it("scrolls to top when new entries arrive and user is near the top", () => {
|
||||
const { rerender, container } = render(<AgentLogViewer entries={[makeEntry({ text: "first" })]} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLDivElement;
|
||||
|
||||
// Simulate user being at the top
|
||||
viewer.scrollTop = 0;
|
||||
|
||||
// Add a new entry
|
||||
rerender(<AgentLogViewer entries={[makeEntry({ text: "second" }), makeEntry({ text: "first" })]} loading={false} />);
|
||||
|
||||
// Should have scrolled to top (newest first)
|
||||
expect(viewer.scrollTop).toBe(0);
|
||||
});
|
||||
|
||||
it("does not auto-scroll when user has scrolled down", () => {
|
||||
const { rerender, container } = render(<AgentLogViewer entries={[makeEntry({ text: "first" })]} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLDivElement;
|
||||
|
||||
// Simulate user scrolling down past the threshold
|
||||
Object.defineProperty(viewer, 'scrollTop', { value: 100, writable: true });
|
||||
|
||||
// Add a new entry
|
||||
rerender(<AgentLogViewer entries={[makeEntry({ text: "second" }), makeEntry({ text: "first" })]} loading={false} />);
|
||||
|
||||
// Should not have scrolled (scrollTop should remain 100)
|
||||
expect(viewer.scrollTop).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user