diff --git a/CHANGELOG.md b/CHANGELOG.md index a7781ba0..c9f3ad57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,11 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Packages without a separate changelog are covered by the cross-package notes below. -## [Unreleased] +## [Unreleased - Patch] + +### Fixed + +- Observer chat and activity now scroll independently within the viewport, display oldest to newest, and preserve reading position when new entries arrive. ## [8.13.0] - 2026-09-25 diff --git a/package-lock.json b/package-lock.json index d0b4c2b6..df0d7917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11652,9 +11652,11 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", + "@testing-library/react": "^16.3.3", "@types/node": "^22.0.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", + "jsdom": "^25.0.1", "postcss": "^8.5.0", "tailwindcss": "^4.0.0", "typescript": "^5.7.0" diff --git a/packages/observer-dashboard/package.json b/packages/observer-dashboard/package.json index 57b97e71..ebc5b868 100644 --- a/packages/observer-dashboard/package.json +++ b/packages/observer-dashboard/package.json @@ -22,9 +22,11 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", + "@testing-library/react": "^16.3.3", "@types/node": "^22.0.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", + "jsdom": "^25.0.1", "postcss": "^8.5.0", "tailwindcss": "^4.0.0", "typescript": "^5.7.0" diff --git a/packages/observer-dashboard/src/components/ActivityLog.tsx b/packages/observer-dashboard/src/components/ActivityLog.tsx index 0cac0781..114c7d80 100644 --- a/packages/observer-dashboard/src/components/ActivityLog.tsx +++ b/packages/observer-dashboard/src/components/ActivityLog.tsx @@ -1,7 +1,8 @@ 'use client'; import { Activity, MessageSquare, Radio, Trash2, UserRound, Wifi, Zap } from 'lucide-react'; -import type { ComponentType } from 'react'; +import { useMemo, useRef, type ComponentType } from 'react'; +import { useFollowLatest } from '../hooks/use-follow-latest'; import { useWebSocketFeed } from '../hooks/use-websocket-feed'; import type { WebSocketFeedCategory, WebSocketFeedEvent } from '../types/dashboard'; import { cn } from '../lib/utils'; @@ -45,6 +46,10 @@ interface ActivityLogProps { export function ActivityLog({ className }: ActivityLogProps) { const { status, events: wsEvents, latestEventAt, clearEvents } = useWebSocketFeed(); + const scrollRef = useRef(null); + const chronologicalEvents = useMemo(() => [...wsEvents].reverse(), [wsEvents]); + useFollowLatest(scrollRef, chronologicalEvents[chronologicalEvents.length - 1]?.id); + const statusClasses = { connected: 'bg-emerald-500/12 text-emerald-300 border-emerald-500/25', connecting: 'bg-amber-500/12 text-amber-300 border-amber-500/25', @@ -53,8 +58,8 @@ export function ActivityLog({ className }: ActivityLogProps) { } as const; return ( -
-
+
+

Activity

@@ -78,7 +83,7 @@ export function ActivityLog({ className }: ActivityLogProps) { {latestEventAt ? `Last WS event ${relativeTime(latestEventAt)}` : 'No WS events yet'}
-
{wsEvents.length === 0 ? : }
+
{wsEvents.length === 0 ? : }
); } diff --git a/packages/observer-dashboard/src/components/ChatFeed.tsx b/packages/observer-dashboard/src/components/ChatFeed.tsx index 1318ef08..c0faf4ba 100644 --- a/packages/observer-dashboard/src/components/ChatFeed.tsx +++ b/packages/observer-dashboard/src/components/ChatFeed.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react'; +import { useFollowLatest } from '../hooks/use-follow-latest'; import { Hash, MessageSquare, UserRound } from 'lucide-react'; import { useMessages, useRelay, sortMessagesChronologically } from '@relaycast/react'; import { MessageCard } from './MessageCard'; @@ -45,8 +46,8 @@ export function ChatFeed({ const showMemberBadge = !!channelName; return ( -
-
+
+
{selectedChannel && !isDm ? ( @@ -68,7 +69,7 @@ export function ChatFeed({
-
+
{channelName ? ( ) : dmId ? ( @@ -84,21 +85,19 @@ export function ChatFeed({ /** * Pagination scaffolding shared by the channel and DM feeds: keeps the viewport * anchored while older messages are prepended, scrolls to the bottom only when - * a new latest message arrives, and loads older pages when the top sentinel - * becomes visible. + * a new latest message arrives while following the bottom, and loads older + * pages when the top sentinel becomes visible. */ function usePaginatedFeed( scrollRef: RefObject, sorted: MessageWithMeta[], loadOlderPage: () => Promise, ) { - const bottomRef = useRef(null); const topRef = useRef(null); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const loadingMoreRef = useRef(false); const prependHeightRef = useRef(null); - const didInitialScrollRef = useRef(false); const newestId = sorted[sorted.length - 1]?.id; const oldestId = sorted[0]?.id; @@ -111,11 +110,7 @@ function usePaginatedFeed( prependHeightRef.current = null; }, [oldestId, scrollRef]); - useEffect(() => { - if (!newestId) return; - bottomRef.current?.scrollIntoView({ behavior: didInitialScrollRef.current ? 'smooth' : 'auto' }); - didInitialScrollRef.current = true; - }, [newestId]); + useFollowLatest(scrollRef, newestId); const loadOlder = useCallback(async () => { if (loadingMoreRef.current) return; @@ -156,7 +151,7 @@ function usePaginatedFeed( return () => observer.disconnect(); }, [showSentinel, loadOlder, scrollRef]); - return { bottomRef, topRef, showSentinel, loadingMore }; + return { topRef, showSentinel, loadingMore }; } function FeedList({ @@ -185,7 +180,6 @@ function FeedList({ const compact = prev !== null && prev.agentName === msg.agentName && new Date(msg.createdAt).getTime() - new Date(prev.createdAt).getTime() < 300000; return ; })} -
); } diff --git a/packages/observer-dashboard/src/components/ConsolePanel.tsx b/packages/observer-dashboard/src/components/ConsolePanel.tsx index b61bf5a8..de69e466 100644 --- a/packages/observer-dashboard/src/components/ConsolePanel.tsx +++ b/packages/observer-dashboard/src/components/ConsolePanel.tsx @@ -5,7 +5,7 @@ import { cn } from '../lib/utils'; export function ConsolePanel({ className }: { className?: string }) { return ( -