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:
gsxdsm
2026-03-31 11:52:02 -07:00
parent 89bd17f06b
commit ca31314be0
2 changed files with 58 additions and 1 deletions

View File

@@ -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={{

View File

@@ -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);
});
});
});